profileShare

rasmusjy / roundtable

Read-only snapshot

No repository description.

main default branch 181 files Expires Sep 13, 2026, 9:06 AM
model-combobox.tsx 3,046 bytes
1 'use client';
2
3 import { Check, ChevronsUpDown, Search } from 'lucide-react';
4 import { useMemo, useState } from 'react';
5 import { Button } from '@/components/ui/button';
6 import { Input } from '@/components/ui/input';
7 import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
8 import { useModels, pricePerMillion, type ModelInfo } from '@/hooks/use-models';
9 import { cn } from '@/lib/utils';
10
11 /** Searchable single-model selector (used for chairman + convergence model). */
12 export function ModelCombobox({
13 value,
14 onChange,
15 placeholder = 'Select a model',
16 exclude,
17 }: {
18 value: string;
19 onChange: (id: string) => void;
20 placeholder?: string;
21 exclude?: string[];
22 }) {
23 const { models } = useModels();
24 const [open, setOpen] = useState(false);
25 const [query, setQuery] = useState('');
26
27 const selected = models.find((m) => m.id === value);
28 const filtered = useMemo(() => filterModels(models, query, exclude), [models, query, exclude]);
29
30 return (
31 <Popover open={open} onOpenChange={setOpen}>
32 <PopoverTrigger asChild>
33 <Button variant="outline" role="combobox" className="w-full justify-between font-normal">
34 <span className="truncate">{selected ? selected.name : value || placeholder}</span>
35 <ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />
36 </Button>
37 </PopoverTrigger>
38 <PopoverContent className="w-[--radix-popover-trigger-width] p-0">
39 <div className="flex items-center gap-2 border-b px-2">
40 <Search className="h-4 w-4 text-muted-foreground" />
41 <Input
42 value={query}
43 onChange={(e) => setQuery(e.target.value)}
44 placeholder="Search models..."
45 className="h-9 border-0 shadow-none focus-visible:ring-0"
46 />
47 </div>
48 <div className="scrollbar-thin max-h-64 overflow-y-auto p-1">
49 {filtered.length === 0 && <div className="p-3 text-sm text-muted-foreground">No models found</div>}
50 {filtered.map((m) => (
51 <button
52 key={m.id}
53 onClick={() => {
54 onChange(m.id);
55 setOpen(false);
56 setQuery('');
57 }}
58 className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm hover:bg-accent"
59 >
60 <Check className={cn('h-4 w-4', value === m.id ? 'opacity-100' : 'opacity-0')} />
61 <span className="flex-1 truncate">{m.name}</span>
62 <span className="text-[11px] text-muted-foreground">{pricePerMillion(m.completionPrice)}</span>
63 </button>
64 ))}
65 </div>
66 </PopoverContent>
67 </Popover>
68 );
69 }
70
71 export function filterModels(models: ModelInfo[], query: string, exclude?: string[]): ModelInfo[] {
72 const q = query.trim().toLowerCase();
73 const ex = new Set(exclude ?? []);
74 return models
75 .filter((m) => !ex.has(m.id))
76 .filter((m) => !q || m.id.toLowerCase().includes(q) || m.name.toLowerCase().includes(q))
77 .slice(0, 100);
78 }
79