import { useEffect, useRef, useState, type CSSProperties } from 'react'; export interface ModelCapability { /** Short id (e.g. 'vision', 'tools', 'streaming'). */ id: string; /** Human-friendly label rendered in the chip. */ label: string; } export interface ModelOption { /** Stable model id (e.g. 'gpt-4o', 'claude-3-5-sonnet'). */ id: string; /** Display name. */ name: string; /** Provider label (e.g. 'OpenAI', 'Anthropic', 'Local'). */ provider: string; /** Optional avatar / glyph for the provider. */ providerGlyph?: string; /** Cost per 1k input tokens (USD), used for sort + display. */ inputCostPer1k?: number; /** Cost per 1k output tokens (USD). */ outputCostPer1k?: number; /** Typical latency ms (P50). */ latencyMs?: number; /** Context window size in tokens. */ contextTokens?: number; /** Capability chips rendered next to the model name. */ capabilities?: ModelCapability[]; /** Mark the model as unavailable (greyed out, unselectable). */ disabled?: boolean; /** Optional short description. */ description?: string; } export interface ModelPickerProps { models: ModelOption[]; value: string; onChange: (modelId: string) => void; /** Show cost cells. Default: true. */ showCost?: boolean; /** Show latency cells. Default: true. */ showLatency?: boolean; /** Trigger label override (default: the active model's name). */ triggerLabel?: string; className?: string; style?: CSSProperties; } /** * `` — dropdown that exposes the full capability profile * of each available LLM. Designed for the top of a chat surface where * power-users want to swap models mid-conversation. * * Shows: provider · name · capability chips · ctx window · cost · latency. * Closes on outside-click and Escape. */ export function ModelPicker({ models, value, onChange, showCost = true, showLatency = true, triggerLabel, className, style, }: ModelPickerProps) { const [open, setOpen] = useState(false); const rootRef = useRef(null); useEffect(() => { if (!open) return; const handler = (e: MouseEvent) => { if (!rootRef.current?.contains(e.target as Node)) setOpen(false); }; const keys = (e: KeyboardEvent) => { if (e.key === 'Escape') setOpen(false); }; document.addEventListener('mousedown', handler); document.addEventListener('keydown', keys); return () => { document.removeEventListener('mousedown', handler); document.removeEventListener('keydown', keys); }; }, [open]); const active = models.find(m => m.id === value); return (
{open && (
{models.map(m => ( ))}
)}
); }