ModelPicker.tsx (3198B)
1 import { useState, useRef, useEffect, useCallback, useMemo } from "react"; 2 3 export interface Model { 4 id: string; 5 name: string; 6 } 7 8 interface ModelPickerProps { 9 models: Model[]; 10 selectedModel: string | null; 11 onSelect?: (modelId: string) => void; 12 } 13 14 function filterModels(models: Model[], query: string): Model[] { 15 if (!query.trim()) return models; 16 const lower = query.toLowerCase(); 17 return models.filter( 18 (m) => 19 m.id.toLowerCase().includes(lower) || 20 m.name.toLowerCase().includes(lower), 21 ); 22 } 23 24 function selectedName(models: Model[], id: string | null): string { 25 if (!id) return "Model"; 26 return models.find((m) => m.id === id)?.name ?? id; 27 } 28 29 export function ModelPicker({ 30 models, 31 selectedModel, 32 onSelect, 33 }: ModelPickerProps) { 34 const [open, setOpen] = useState(false); 35 const [search, setSearch] = useState(""); 36 const ref = useRef<HTMLDivElement>(null); 37 38 const filtered = useMemo(() => filterModels(models, search), [models, search]); 39 40 const handleClickOutside = useCallback((e: MouseEvent) => { 41 if (ref.current && !ref.current.contains(e.target as Node)) { 42 setOpen(false); 43 } 44 }, []); 45 46 useEffect(() => { 47 document.addEventListener("mousedown", handleClickOutside); 48 return () => document.removeEventListener("mousedown", handleClickOutside); 49 }, [handleClickOutside]); 50 51 const handleSelect = useCallback( 52 (id: string) => { 53 onSelect?.(id); 54 setOpen(false); 55 setSearch(""); 56 }, 57 [onSelect], 58 ); 59 60 return ( 61 <div ref={ref} className="relative"> 62 <button 63 type="button" 64 onClick={() => setOpen(!open)} 65 className="rounded-lg border border-gray-700 bg-gray-900 px-3 py-1.5 text-xs text-gray-300 hover:border-gray-600 transition-colors" 66 > 67 {selectedName(models, selectedModel)} 68 </button> 69 70 {open && ( 71 <div className="absolute bottom-full left-0 z-10 mb-1 w-56 rounded-lg border border-gray-700 bg-gray-900 shadow-lg"> 72 <div className="p-2"> 73 <input 74 type="text" 75 placeholder="Search models..." 76 value={search} 77 onChange={(e) => setSearch(e.target.value)} 78 className="w-full rounded border border-gray-700 bg-gray-800 px-2 py-1 text-xs text-gray-200 placeholder-gray-500 focus:outline-none focus:border-gray-500" 79 /> 80 </div> 81 <div className="max-h-48 overflow-y-auto"> 82 {filtered.map((model) => ( 83 <button 84 key={model.id} 85 type="button" 86 onClick={() => handleSelect(model.id)} 87 className={`block w-full px-3 py-2 text-left text-xs transition-colors ${ 88 model.id === selectedModel 89 ? "bg-blue-600/20 text-blue-300" 90 : "text-gray-300 hover:bg-gray-800" 91 }`} 92 > 93 {model.name} 94 </button> 95 ))} 96 {filtered.length === 0 && ( 97 <div className="px-3 py-2 text-xs text-gray-500"> 98 No models found 99 </div> 100 )} 101 </div> 102 </div> 103 )} 104 </div> 105 ); 106 }