theo-agent-dashboard

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

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 }