profileShare

rasmusjy / roundtable

Read-only snapshot

No repository description.

main default branch 181 files Expires Sep 13, 2026, 9:06 AM
model-picker.tsx 3,993 bytes
1 'use client';
2
3 import { Plus, RotateCcw, Search, X } from 'lucide-react';
4 import { useMemo, useState } from 'react';
5 import { filterModels } from '@/components/debate/model-combobox';
6 import { Badge } from '@/components/ui/badge';
7 import { Button } from '@/components/ui/button';
8 import { Input } from '@/components/ui/input';
9 import { displayNameForModel } from '@/core/types';
10 import { useModels, pricePerMillion } from '@/hooks/use-models';
11 import { participantColor, participantTag } from '@/lib/model-visuals';
12
13 /** Multi-select council picker (3-6 models), with search and live pricing. */
14 export function ModelPicker({
15 council,
16 onChange,
17 max = 6,
18 }: {
19 council: string[];
20 onChange: (ids: string[]) => void;
21 max?: number;
22 }) {
23 const { models, loading, error, reload } = useModels();
24 const [query, setQuery] = useState('');
25
26 const filtered = useMemo(() => filterModels(models, query, council), [models, query, council]);
27 const atMax = council.length >= max;
28
29 const add = (id: string) => {
30 if (!atMax && !council.includes(id)) onChange([...council, id]);
31 };
32 const remove = (id: string) => onChange(council.filter((m) => m !== id));
33
34 return (
35 <div className="space-y-3">
36 <div className="flex flex-wrap gap-2">
37 {council.length === 0 && (
38 <p className="text-sm text-muted-foreground">Pick 3-6 models to form the council.</p>
39 )}
40 {council.map((id, i) => (
41 <Badge key={id} variant="outline" className="gap-1.5 py-1 pl-1.5 pr-1">
42 <span
43 className="flex h-4 w-4 items-center justify-center rounded text-[9px] font-bold text-white"
44 style={{ backgroundColor: participantColor(i) }}
45 >
46 {participantTag(i)}
47 </span>
48 {displayNameForModel(id)}
49 <button onClick={() => remove(id)} className="rounded p-0.5 hover:bg-muted" aria-label="Remove">
50 <X className="h-3 w-3" />
51 </button>
52 </Badge>
53 ))}
54 <Badge variant="secondary">
55 {council.length}/{max}
56 </Badge>
57 </div>
58
59 <div className="rounded-lg border">
60 <div className="flex items-center gap-2 border-b px-2">
61 <Search className="h-4 w-4 text-muted-foreground" />
62 <Input
63 value={query}
64 onChange={(e) => setQuery(e.target.value)}
65 placeholder={loading ? 'Loading models...' : 'Search 300+ models...'}
66 className="h-9 border-0 shadow-none focus-visible:ring-0"
67 />
68 </div>
69 <div className="scrollbar-thin max-h-56 overflow-y-auto p-1">
70 {filtered.map((m) => (
71 <button
72 key={m.id}
73 onClick={() => add(m.id)}
74 disabled={atMax}
75 className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm hover:bg-accent disabled:opacity-40"
76 >
77 <Plus className="h-3.5 w-3.5 text-muted-foreground" />
78 <span className="min-w-0 flex-1 truncate">{m.name}</span>
79 <span className="hidden font-mono text-[10px] text-muted-foreground sm:inline">{m.id}</span>
80 <span className="text-[11px] text-muted-foreground">
81 in {pricePerMillion(m.promptPrice)} ยท out {pricePerMillion(m.completionPrice)}
82 </span>
83 </button>
84 ))}
85 {error && models.length === 0 ? (
86 <div className="flex items-center justify-between gap-2 p-3 text-sm">
87 <span className="text-muted-foreground">Could not load the model catalog.</span>
88 <Button variant="outline" size="sm" onClick={reload}>
89 <RotateCcw className="h-3.5 w-3.5" /> Retry
90 </Button>
91 </div>
92 ) : (
93 !loading &&
94 filtered.length === 0 && <div className="p-3 text-sm text-muted-foreground">No models match.</div>
95 )}
96 </div>
97 </div>
98 </div>
99 );
100 }
101