import { useState, useEffect, useRef } from "react"; import { createPortal } from "react-dom"; import { Cpu, ChevronDown, Check } from "lucide-react"; import { motion, AnimatePresence } from "framer-motion"; import axios from "axios"; const FALLBACK_MODELS = ["llama3", "mistral", "llama3.1", "phi3"]; export default function ModelSelector({ value, onChange }) { const [models, setModels] = useState(FALLBACK_MODELS); const [isOpen, setIsOpen] = useState(false); const [dropdownStyle, setDropdownStyle] = useState({}); const buttonRef = useRef(null); useEffect(() => { fetchModels(); }, []); useEffect(() => { if (!isOpen) return; const handleClose = () => setIsOpen(false); document.addEventListener("mousedown", handleOutsideClick); document.addEventListener("scroll", handleClose, true); window.addEventListener("resize", handleClose); return () => { document.removeEventListener("mousedown", handleOutsideClick); document.removeEventListener("scroll", handleClose, true); window.removeEventListener("resize", handleClose); }; }, [isOpen]); const handleOutsideClick = (e) => { if (buttonRef.current && !buttonRef.current.contains(e.target)) { // Check if click is inside the portal dropdown const portal = document.getElementById("model-selector-portal"); if (portal && portal.contains(e.target)) return; setIsOpen(false); } }; const calculatePosition = () => { if (!buttonRef.current) return {}; const rect = buttonRef.current.getBoundingClientRect(); const dropdownWidth = 200; const dropdownHeight = 280; const viewportH = window.innerHeight; const viewportW = window.innerWidth; // Horizontal: align to right edge of button, clamp to viewport let left = rect.right - dropdownWidth; if (left < 8) left = 8; if (left + dropdownWidth > viewportW - 8) { left = viewportW - dropdownWidth - 8; } // Vertical: prefer below, flip above if not enough space const spaceBelow = viewportH - rect.bottom; const spaceAbove = rect.top; let top; let transformOrigin; if (spaceBelow >= dropdownHeight || spaceBelow >= spaceAbove) { // Open downward top = rect.bottom + 6; transformOrigin = "top right"; } else { // Open upward top = rect.top - dropdownHeight - 6; transformOrigin = "bottom right"; } return { position: "fixed", top: `${top}px`, left: `${left}px`, width: `${dropdownWidth}px`, zIndex: 99999, transformOrigin, }; }; const handleToggle = () => { if (!isOpen) { setDropdownStyle(calculatePosition()); } setIsOpen((prev) => !prev); }; const fetchModels = async () => { try { const { data } = await axios.get("http://localhost:11434/api/tags", { timeout: 3000, }); const names = data.models?.map((m) => m.name) || []; if (names.length > 0) setModels(names); } catch { // Silent fallback } }; const selectedLabel = value || "llama3"; // The dropdown rendered via portal at body level const DropdownPortal = () => createPortal( {isOpen && ( {/* Header */} Available Models {/* Model list */} {models.map((model) => ( { onChange(model); setIsOpen(false); }} className="w-full flex items-center justify-between gap-2 px-2.5 py-2 rounded-lg text-left text-xs text-nexus-text hover:bg-nexus-surface transition-colors" > {model} {value === model && ( )} ))} {/* Footer */} Powered by Ollama )} , document.body, ); return ( <> {/* Trigger Button */} {selectedLabel} > ); }
Available Models
Powered by Ollama