import { useState, useRef, useEffect } from "react"; import { Bot, Brain, ChevronDown, Cpu, Gem, Loader2, Search, Sparkles, Wind } from "lucide-react"; import type { ModelInfo, ServerStatus } from "../lib/types"; import type { RuntimeModelFile } from "../features/runtime/types"; import { getDisplayName } from "../lib/modelAlias"; interface ModelSelectorPillProps { models: ModelInfo[]; localModels: RuntimeModelFile[]; selectedModel: string; onModelChange: (id: string) => void; onModelSwap?: (path: string) => void; isSwapping?: boolean; status: ServerStatus; } export default function ModelSelectorPill({ models, localModels, selectedModel, onModelChange, onModelSwap, isSwapping, status, }: ModelSelectorPillProps) { const [dropdownOpen, setDropdownOpen] = useState(false); const [openDirection, setOpenDirection] = useState<"up" | "down">("down"); const dropdownRef = useRef(null); useEffect(() => { if (!dropdownOpen) return; const handleClickOutside = (e: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { setDropdownOpen(false); } }; const el = dropdownRef.current; if (el) { const rect = el.getBoundingClientRect(); const estimatedMenuHeight = 260; const spaceBelow = window.innerHeight - rect.bottom; const spaceAbove = rect.top; setOpenDirection(spaceBelow >= estimatedMenuHeight || spaceBelow >= spaceAbove ? "down" : "up"); } document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [dropdownOpen]); const handleSelectModel = (modelId: string) => { setDropdownOpen(false); onModelChange(modelId); // Only call onModelSwap for local models (which have file paths) // API models don't have paths and shouldn't trigger a model swap const isLocalModel = localModels.some((m) => m.path === modelId); if (onModelSwap && isLocalModel) { onModelSwap(modelId); } }; const getModelIcon = (label: string) => { const l = label.toLowerCase(); if (l.includes("qwen")) return ; if (l.includes("deepseek")) return ; if (l.includes("mistral")) return ; if (l.includes("gemma") || l.includes("gemini")) return ; if (l.includes("phi")) return ; if (l.includes("llama")) return ; return ; }; // Unified model list: merge API models + local scanned files const allAvailableModels = [ ...models.map((m) => ({ id: m.id, label: getDisplayName(m.id) })), ...localModels.map((m) => ({ id: m.path, label: getDisplayName(m.path) })), ]; // Deduplicate by label (display name) - same name means same model const seenLabels = new Set(); const dedupedModels = allAvailableModels.filter((m) => { if (seenLabels.has(m.label)) return false; seenLabels.add(m.label); return true; }); // Show all available models in the selector const selectedLabel = getDisplayName(selectedModel) || "No model"; return (
{dropdownOpen && (
    {dedupedModels.map((m) => (
  • handleSelectModel(m.id)} > {m.label}
  • ))}
)}
); }