"use client"; import { BrainIcon, CheckIcon, ChevronDownIcon } from "lucide-react"; import { AnimatePresence, motion, useReducedMotion, useSpring, useTransform } from "motion/react"; import { type KeyboardEvent, useEffect, useId, useRef, useState } from "react"; import { Popover, PopoverPopup, PopoverTrigger } from "@/components/ui/popover"; import { Slider } from "@/components/ui/slider"; import { Switch } from "@/components/ui/switch"; import { cn } from "@/lib/utils"; export interface SwitcherModel { id: string; name: string; tagline: string; speed: number; quality: number; cost: number; latency: number; price: number; thinking?: boolean; badge?: string; } export interface ModelSwitcherValue { model: string; thinking: boolean; effort: number; } export interface ModelSwitcherPanelProps { models?: SwitcherModel[]; value?: ModelSwitcherValue; defaultValue?: Partial; onValueChange?: (value: ModelSwitcherValue) => void; className?: string; } const defaultModels: SwitcherModel[] = [ { id: "quill-4", name: "Quill 4", tagline: "Everyday writing and code", speed: 0.72, quality: 0.78, cost: 0.45, latency: 380, price: 3, thinking: true, badge: "Default", }, { id: "quill-4-lite", name: "Quill 4 Lite", tagline: "Snappy answers, tiny bills", speed: 0.95, quality: 0.55, cost: 0.12, latency: 160, price: 0.4, }, { id: "meridian-ultra", name: "Meridian Ultra", tagline: "Hard problems, long context", speed: 0.38, quality: 0.97, cost: 0.9, latency: 820, price: 15, thinking: true, }, ]; const efforts = ["Low", "Medium", "High", "Max"]; const spring = { type: "spring", stiffness: 420, damping: 36 } as const; const easeOut = [0.23, 1, 0.32, 1] as const; const resolve = (models: SwitcherModel[], v?: Partial): ModelSwitcherValue => ({ model: v?.model ?? models[0]?.id ?? "", thinking: v?.thinking ?? false, effort: v?.effort ?? 1, }); export function ModelSwitcherPanel({ models = defaultModels, value: valueProp, defaultValue, onValueChange, className, }: ModelSwitcherPanelProps) { const reduce = useReducedMotion(); const groupId = useId(); const [inner, setInner] = useState(() => resolve(models, defaultValue)); const value = valueProp ?? inner; const [hovered, setHovered] = useState(null); const rowRefs = useRef<(HTMLButtonElement | null)[]>([]); const selected = models.find((m) => m.id === value.model) ?? models[0]; const shown = models.find((m) => m.id === hovered) ?? selected; const canThink = Boolean(selected?.thinking); const thinking = canThink && value.thinking; const update = (patch: Partial) => { const next = { ...value, ...patch }; if (!valueProp) setInner(next); onValueChange?.(next); }; const latency = (selected?.latency ?? 0) * (thinking ? 1.6 + value.effort * 0.9 : 1); const price = selected?.price ?? 0; const onKey = (e: KeyboardEvent, i: number) => { const dir = e.key === "ArrowDown" ? 1 : e.key === "ArrowUp" ? -1 : 0; if (!dir) return; e.preventDefault(); const next = (i + dir + models.length) % models.length; const m = models[next]; if (m) update({ model: m.id }); rowRefs.current[next]?.focus(); }; return (
setHovered(null)} role="radiogroup" > {models.map((m, i) => { const on = m.id === value.model; return ( ); })}
{thinking && (
Effort {efforts[value.effort]}
"Thinking effort"} max={efforts.length - 1} min={0} onValueChange={(v) => update({ effort: Array.isArray(v) ? (v[0] ?? 0) : v })} step={1} value={value.effort} />
{efforts.map((e) => ( {e} ))}
)}
First token{" "} ~ (v >= 1000 ? `${(v / 1000).toFixed(1)}s` : `${Math.round(v)}ms`)} value={latency} /> `$${v.toFixed(2)}`} value={price} /> {" "} / 1M tokens
); } function Bar({ label, value, tone }: { label: string; value: number; tone: string }) { return ( <> {label} {Array.from({ length: 5 }, (_, i) => { const fill = Math.max(0, Math.min(1, value * 5 - i)); return ( ); })} ); } function Rolling({ value, format }: { value: number; format: (v: number) => string }) { const reduce = useReducedMotion(); const mv = useSpring(value, { stiffness: 200, damping: 30 }); const text = useTransform(mv, format); useEffect(() => { if (reduce) mv.jump(value); else mv.set(value); }, [value, reduce, mv]); return {text}; } export interface ModelSwitcherProps extends ModelSwitcherPanelProps { defaultOpen?: boolean; } export function ModelSwitcher({ models = defaultModels, defaultOpen, value, defaultValue, onValueChange, className }: ModelSwitcherProps) { const [inner, setInner] = useState(() => resolve(models, defaultValue)); const current = value ?? inner; const model = models.find((m) => m.id === current.model); return ( {model?.name} {current.thinking && model?.thinking && } { if (!value) setInner(v); onValueChange?.(v); }} value={current} /> ); }