Model picker Rows that describe the job rather than the architecture: what it is for, what it costs relative to the cheap one, how much context it holds and what it accepts. Switching mid-conversation leaves a marker, because the turns above came from something else. npx shadcn@latest add dropdown-menu button npm i lucide-react Tokens this needs: --card, --card-foreground, --popover, --popover-foreground, --accent, --accent-foreground, --muted, --muted-foreground, --border, --chart-1 ──────────────────────────────────────────────────────────────────────── // ModelPicker.tsx ──────────────────────────────────────────────────────────────────────── import { useState } from "react"; import { Check, ChevronDown } from "lucide-react"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; /** * Each row describes the job, not the architecture. A parameter count and a * point release tell a chooser nothing they can act on; "analysis and code" * and "8×" do. Cost, window and modality sit in the list because they change * which tasks are possible, and hiding them makes the menu decor. */ export const MODELS = [ { id: "fast", name: "Fast", job: "everyday questions", cost: "1×", meta: "text only" }, { id: "careful", name: "Careful", job: "analysis and code", cost: "8×", meta: "text, images" }, { id: "long", name: "Long context", job: "whole documents", cost: "6×", meta: "1 million tokens" }, ] as const; export type ModelId = (typeof MODELS)[number]["id"]; type Props = { /** "auto" lets the system route by the question. Most people never open * this menu, so auto and its routing decide the experience. */ value: ModelId | "auto"; /** What auto picked for the current turn, so the choice is visible and can * be overridden rather than silent. */ routed?: ModelId; onChange: (id: ModelId | "auto") => void; /** Start open. Used when the picker is the thing on screen, as in a demo. */ defaultOpen?: boolean; }; export function ModelPicker({ value, routed, onChange, defaultOpen = false }: Props) { const [open, setOpen] = useState(defaultOpen); const current = MODELS.find((m) => m.id === value); const picked = MODELS.find((m) => m.id === routed); return ( // Non-modal: a picker beside a composer should not lock the page behind it. {MODELS.map((m) => ( onChange(m.id)} className="flex items-start gap-3 px-3 py-2" >
{m.name}
{m.job}
{m.cost}
{m.meta}
))} onChange("auto")} className="flex items-start gap-3 px-3 py-2" >
Auto
auto-routed by the question{picked ? `, picked ${picked.name}` : ""}
); } /** * Sits on the turn where the model changed. The new model inherits the * transcript, and without this the change of voice reads as losing track. */ export function ModelSwitchMarker({ to }: { to: ModelId }) { const m = MODELS.find((x) => x.id === to)!; return (
switched here to {m.name}

earlier turns came from another model

); } ──────────────────────────────────────────────────────────────────────── // demo.tsx ──────────────────────────────────────────────────────────────────────── import { useState } from "react"; import { ModelPicker, ModelSwitchMarker, type ModelId } from "./ModelPicker"; /** * Opens on arrival, because the menu is the thing being shown. Auto is the * default and it picked Careful for this turn. Choosing a model records a * switch marker below. */ export default function Demo() { const [value, setValue] = useState("auto"); const [switched, setSwitched] = useState(null); return (
{ setValue(id); if (id !== "auto") setSwitched(id); }} />
{switched && }
); }