/** * VersionSelector * * Shows the active API version badge ("Version 2") with a chevron icon. * Clicking opens a submenu listing all available versions: * - Active version: shown with a check mark * - Downloaded but inactive: "Switch" button * - Not yet downloaded: Download icon button → triggers server-side download * then auto-switches when ready */ import { useCallback, useEffect, useRef, useState } from "react"; import { ChevronRight, Download, Check, RefreshCw, AlertCircle, Layers, } from "lucide-react"; import { fetchVersions, loadVersion, VersionInfo } from "../api"; interface Props { activeVersion: "v1" | "v2" | "v3"; onSwitch: (v: "v1" | "v2" | "v3") => void; } export default function VersionSelector({ activeVersion, onSwitch }: Props) { const [open, setOpen] = useState(false); const [versions, setVersions] = useState([]); const [busy, setBusy] = useState(null); // version being downloaded const menuRef = useRef(null); const pollRef = useRef | null>(null); // ── Load version list ──────────────────────────────────────────────────── const refresh = useCallback(() => { fetchVersions() .then(setVersions) .catch(() => {}); }, []); useEffect(() => { refresh(); }, [refresh]); // ── Poll while a download is in progress ──────────────────────────────── useEffect(() => { const hasDownloading = versions.some((v) => v.status === "downloading"); if (hasDownloading && !pollRef.current) { pollRef.current = setInterval(refresh, 2500); } if (!hasDownloading && pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; setBusy(null); } return () => { if (pollRef.current) clearInterval(pollRef.current); }; }, [versions, refresh]); // ── Close on outside click ─────────────────────────────────────────────── useEffect(() => { if (!open) return; const handler = (e: MouseEvent) => { if (menuRef.current && !menuRef.current.contains(e.target as Node)) { setOpen(false); } }; document.addEventListener("mousedown", handler); return () => document.removeEventListener("mousedown", handler); }, [open]); // ── Actions ────────────────────────────────────────────────────────────── const handleDownload = async (version: string) => { setBusy(version); try { await loadVersion(version); refresh(); // poll will take over } catch { setBusy(null); } }; const handleSwitch = (version: string) => { onSwitch(version as "v1" | "v2" | "v3"); setOpen(false); }; const activeDisplay = versions.find((v) => v.id === activeVersion)?.display ?? `Version ${activeVersion[1]}`; // Versions that are NOT the active one const others = versions.filter((v) => v.id !== activeVersion); return (
{/* Badge button */} {/* Dropdown */} {open && (
{/* Header */}
Model Versions
{/* Active version row */}
{activeDisplay} active
{/* Other versions */} {others.length === 0 && (
No other versions available
)} {others.map((v) => { const isDownloading = v.status === "downloading" || busy === v.id; const isError = v.status === "error"; const canSwitch = v.loaded && !isDownloading; return (
{v.display} {v.loaded && v.model_count > 0 && ( {v.model_count} models )} {isError && ( error )} {isDownloading && ( downloading… )}
{/* Switch button — visible when loaded */} {canSwitch && ( )} {/* Download button — visible when NOT loaded and NOT downloading */} {!v.loaded && !isDownloading && !isError && ( )} {/* Spinner while downloading */} {isDownloading && ( )} {/* Error retry */} {isError && !isDownloading && ( )}
); })} {/* Footer hint */}
Models hosted on Hugging Face Hub
)}
); }