import { useRef, useState, useEffect, useCallback } from 'react'; import { useModelStore } from '../store/useModelStore'; import { useModelSearchStore } from '../store/useModelSearchStore'; import { useScanStore } from '../store/useScanStore'; import { useLocaleStore } from '../store/useLocaleStore'; import { useDebounce } from '../hooks/useDebounce'; import type { TranslationKey } from '../i18n/translations'; import type { HubSearchResult } from '../types/settings'; import type { ModelListEntry } from '../api/client'; function formatDownloads(n: number): string { if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`; if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`; return String(n); } function TlBadge({ compat }: { compat: boolean | null }) { const t = useLocaleStore((s) => s.t); if (compat === true) { return ( {t('modelPicker.tlCompat' as TranslationKey)} ); } if (compat === false) { return ( {t('modelPicker.tlIncompat' as TranslationKey)} ); } return ( {t('modelPicker.tlUnknown' as TranslationKey)} ); } export function ModelPicker() { const [open, setOpen] = useState(false); const ref = useRef(null); const { modelInfo, isLoading, availableModels, loadModel } = useModelStore(); const { query, setQuery, results, isSearching, search, recentModels, clearResults, tlOnly, setTlOnly } = useModelSearchStore(); const addLog = useScanStore((s) => s.addLog); const t = useLocaleStore((s) => s.t); // Close on outside click useEffect(() => { if (!open) return; const handler = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) { setOpen(false); } }; document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, [open]); // Debounced search const debouncedSearch = useDebounce( useCallback((q: string) => search(q), [search]), 300, ); const handleQueryChange = (e: React.ChangeEvent) => { const val = e.target.value; setQuery(val); debouncedSearch(val); }; const handleSelect = async (modelId: string) => { setOpen(false); clearResults(); addLog(`Loading model: ${modelId}...`); await loadModel(modelId); const info = useModelStore.getState().modelInfo; if (info) { addLog(`Model loaded: ${modelId}`); } }; // Registry models from available list const registryModels = availableModels.filter( (m) => !m.source || m.source === 'registry', ); // Recent models that aren't in the registry const recentIds = recentModels.filter( (id) => !registryModels.some((m) => m.model_id === id), ); const currentName = modelInfo?.model_name || modelInfo?.model_id || 'gpt2'; const rowStyle = { display: 'block' as const, width: '100%', textAlign: 'left' as const, background: 'none', border: 'none', padding: '6px 12px', cursor: 'pointer', fontFamily: 'var(--font-primary)', }; const renderRegistryRow = (m: ModelListEntry) => ( ); const renderSearchRow = (r: HubSearchResult) => ( ); const sectionLabel = (text: string) => (
{text}
); return (
{open && (
{/* Search Input */}
{isSearching && ( ... )}
{/* Recommended (registry) models */} {registryModels.length > 0 && (
{sectionLabel(t('modelPicker.recommended' as TranslationKey))} {registryModels.map(renderRegistryRow)}
)} {/* Recent models */} {recentIds.length > 0 && (
{sectionLabel(t('modelPicker.recent' as TranslationKey))} {recentIds.map((id) => ( ))}
)} {/* Search Results */} {results.length > 0 && (
{sectionLabel(t('modelPicker.results' as TranslationKey))} {results.map(renderSearchRow)}
)} {/* No results */} {query.trim() && !isSearching && results.length === 0 && (
{t('modelPicker.noResults' as TranslationKey)}
)}
)}
); }