'use client'; import { useEffect, useMemo, useRef, useState } from 'react'; import { useTranslations } from 'next-intl'; import { Check, ChevronDown, Cpu, Sparkles } from 'lucide-react'; import { DEFAULT_MODEL_ID, fetchAvailableModels, getSelectedModelId, setSelectedModelId, subscribeSelectedModel, type ModelOption, } from '@/lib/models'; /** * Header-mounted model picker. Drives the `?model=` param appended to * every `POST /api/v1/inspect` call (see lib/api.ts). * * - Loads the catalog from `GET /api/v1/models`; falls back to a hardcoded * list when the endpoint is offline (parallel backend work). * - Selection persists in localStorage. * - A subtle "demo" badge on the trigger makes this prominent on stage. */ export function ModelSelector() { const t = useTranslations('nav.modelSelector'); const [models, setModels] = useState([]); const [selectedId, setSelectedId] = useState(() => getSelectedModelId(), ); const [open, setOpen] = useState(false); const [loading, setLoading] = useState(true); const rootRef = useRef(null); // Initial catalog fetch + react to cross-tab changes. useEffect(() => { const ac = new AbortController(); (async () => { const items = await fetchAvailableModels({ signal: ac.signal }); setModels(items); setLoading(false); // If our cached id isn't in the catalog, fall back to the first item // (preferring `recommended`) and persist the correction. const current = getSelectedModelId(); if (!items.find((m) => m.id === current)) { const next = items.find((m) => m.recommended)?.id ?? items[0]?.id ?? DEFAULT_MODEL_ID; setSelectedModelId(next); setSelectedId(next); } })(); const unsub = subscribeSelectedModel((id) => setSelectedId(id)); return () => { ac.abort(); unsub(); }; }, []); // Close on outside click / Esc. useEffect(() => { if (!open) return; const onClick = (e: MouseEvent) => { if (rootRef.current && !rootRef.current.contains(e.target as Node)) { setOpen(false); } }; const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') setOpen(false); }; document.addEventListener('mousedown', onClick); document.addEventListener('keydown', onKey); return () => { document.removeEventListener('mousedown', onClick); document.removeEventListener('keydown', onKey); }; }, [open]); const selected = useMemo( () => models.find((m) => m.id === selectedId) ?? null, [models, selectedId], ); function pick(id: string) { setSelectedModelId(id); setSelectedId(id); setOpen(false); } const triggerLabel = selected ? selected.kind === 'pretrained' ? t('pretrained') : t('custom') : t('label'); const TriggerIcon = selected?.kind === 'pretrained' ? Cpu : Sparkles; return (
{open && (

{t('heading')}

{t('hint')}

    {loading && models.length === 0 && (
  • {t('loading')}
  • )} {models.map((m) => { const active = m.id === selectedId; const Icon = m.kind === 'pretrained' ? Cpu : Sparkles; return (
  • ); })}
)}
); }