'use client'; import { Search } from 'lucide-react'; import { useEffect, useId, useRef, useState } from 'react'; import { clientApi } from '@/lib/client-api'; import { cn } from '@/lib/cn'; import type { Suggestion } from '@/lib/types'; /** * Model autocomplete on `/search/suggest` (type model), used by the calculator. Emits the chosen slug through `onSelect`; * works as a plain text field when JS is slow (the slug can be typed). */ const DEFAULT_TYPES = ['model']; export function ModelPicker({ value, label, onSelect, placeholder = 'Type a model name…', className, inputClassName, types = DEFAULT_TYPES }: { value: string; label?: string | null; onSelect: (slug: string, name: string) => void; placeholder?: string; className?: string; inputClassName?: string; types?: string[] }) { const [q, setQ] = useState(label ?? value); const [items, setItems] = useState([]); const [open, setOpen] = useState(false); const [active, setActive] = useState(0); const id = useId(); const box = useRef(null); useEffect(() => { setQ(label ?? value); }, [label, value]); useEffect(() => { if (!open || q.trim().length < 2 || q === label) { setItems([]); return; } const ctrl = new AbortController(); const t = setTimeout(() => { clientApi .suggest(q.trim(), ctrl.signal) .then((r) => setItems(r.items.filter((i) => types.includes(i.entity_type)).slice(0, 8))) .catch(() => setItems([])); }, 120); return () => { clearTimeout(t); ctrl.abort(); }; }, [q, open, label, types]); useEffect(() => { const onDoc = (e: MouseEvent) => { if (!box.current?.contains(e.target as Node)) setOpen(false); }; document.addEventListener('mousedown', onDoc); return () => document.removeEventListener('mousedown', onDoc); }, []); const pick = (s: Suggestion) => { onSelect(s.slug, s.name); setQ(s.name); setOpen(false); }; return (
{ setQ(e.target.value); setOpen(true); setActive(0); }} onFocus={() => setOpen(true)} onKeyDown={(e) => { if (!open || !items.length) { if (e.key === 'Enter' && q.trim() && !items.length) { onSelect(q.trim().toLowerCase().replace(/\s+/g, '-'), q.trim()); setOpen(false); } return; } if (e.key === 'ArrowDown') { e.preventDefault(); setActive((a) => (a + 1) % items.length); } else if (e.key === 'ArrowUp') { e.preventDefault(); setActive((a) => (a - 1 + items.length) % items.length); } else if (e.key === 'Enter') { e.preventDefault(); const s = items[active]; if (s) pick(s); } else if (e.key === 'Escape') setOpen(false); }} placeholder={placeholder} role="combobox" aria-expanded={open && items.length > 0} aria-controls={`${id}-list`} aria-autocomplete="list" autoComplete="off" className={cn('h-11 w-full border border-rule bg-surface pl-9 pr-2.5 text-[15px] text-ink focus:border-accent focus:outline-none', inputClassName)} data-model-picker />
{open && items.length > 0 && (
    {items.map((s, i) => (
  • ))}
)}
); }