All blocks
Model Picker
Settings menu for choosing an inference model — Fast switch, Effort submenu, and a searchable combobox for the model list.
Install
1npx stylexblocks add model-pickerSource
components/blocks/base-ui/model-picker.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321import * as stylex from "@stylexjs/stylex";import { useState } from "react";import { Button } from "#components/ui/base-ui/button.tsx";import { Combobox } from "#components/ui/base-ui/combobox.tsx";import { Menu } from "#components/ui/base-ui/menu.tsx";import { Switch } from "#components/ui/base-ui/switch.tsx";import { semanticColors } from "@stylexblocks/ui/semantic-tokens/colors.stylex.ts";import { accentPalette } from "@stylexblocks/ui/themes/palettes.ts";import type { StyleOverrideProps } from "#lib/recipe-props.ts";
export type Effort = "low" | "medium" | "high";
export interface Model { /** Stable id on the wire, e.g. "cursor-grok-4.6". */ value: string; /** Display name shown on the trigger and in the list. */ label: string;}
export const models: Model[] = [ { value: "auto", label: "Auto" }, { value: "cursor-grok-4.6", label: "Cursor Grok 4.6" }, { value: "composer-2.5", label: "Composer 2.5" }, { value: "claude-opus-4.8", label: "Claude Opus 4.8" }, { value: "gpt-5.6-sol", label: "GPT-5.6 Sol" }, { value: "claude-fable-5", label: "Claude Fable 5" },];
export const efforts: { value: Effort; label: string }[] = [ { value: "low", label: "Low" }, { value: "medium", label: "Medium" }, { value: "high", label: "High" },];
function tagsForSettings(effort: Effort, fast: boolean): string[] { const tags = [efforts.find((item) => item.value === effort)?.label ?? "High"]; if (fast) tags.push("Fast"); return tags;}
const styles = stylex.create({ comboRoot: { width: "100%", }, labelRow: { display: "flex", alignItems: "center", flex: "none", gap: "0.375rem", minWidth: 0, }, itemText: { flex: "none", }, tags: { color: semanticColors.fgSubtle, display: "flex", flexShrink: 0, gap: "0.375rem", }, switchHit: { display: "inline-flex", marginInlineStart: "auto", }, menu: { minWidth: "13.5rem", }, modelMenu: { minWidth: 220, }, search: { paddingBlock: 0, },});
function ChevronRightIcon() { return ( <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"> <path d="m6 4 4 4-4 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> </svg> );}
export interface ModelPickerProps extends StyleOverrideProps { /** Override the offered models. */ items?: Model[]; /** Model id of the initial selection. */ defaultValue?: string; /** Model id of the controlled selection. */ value?: string; onValueChange?: (id: string) => void; defaultFast?: boolean; fast?: boolean; onFastChange?: (fast: boolean) => void; defaultEffort?: Effort; effort?: Effort; onEffortChange?: (effort: Effort) => void; /** Called when the panel footer action is pressed. */ onAddModels?: () => void; /** Posts the selected model id with native form submits. */ name?: string; disabled?: boolean; /** Open the settings menu on mount — for catalog previews. */ defaultOpen?: boolean; /** Open the model list on mount — for catalog previews. */ defaultModelOpen?: boolean; searchPlaceholder?: string; addLabel?: string;}
/** * ModelPicker — settings menu for choosing an inference model. Fast is a * switch, Effort is a nested menu, and Model is a hover submenu that embeds * a searchable combobox in the submenu content. */export function ModelPicker({ items = models, defaultValue = "cursor-grok-4.6", value, onValueChange, defaultFast = true, fast, onFastChange, defaultEffort = "high", effort, onEffortChange, onAddModels, name, disabled, defaultOpen = false, defaultModelOpen = false, searchPlaceholder = "Search models", addLabel = "Add Models", style,}: ModelPickerProps) { const [innerModel, setInnerModel] = useState(defaultValue); const [innerFast, setInnerFast] = useState(defaultFast); const [innerEffort, setInnerEffort] = useState(defaultEffort); const [menuOpen, setMenuOpen] = useState(defaultOpen); const [modelOpen, setModelOpen] = useState(defaultModelOpen); const [query, setQuery] = useState("");
const selectedId = value ?? innerModel; const fastOn = fast ?? innerFast; const effortValue = effort ?? innerEffort; const selected = items.find((m) => m.value === selectedId) ?? items[0]; const effortLabel = efforts.find((e) => e.value === effortValue)?.label ?? "High"; const modelTags = tagsForSettings(effortValue, fastOn); const selectedModel = items.find((m) => m.value === selectedId) ?? undefined;
const setModel = (id: string) => { setInnerModel(id); onValueChange?.(id); }; const setFast = (next: boolean) => { setInnerFast(next); onFastChange?.(next); }; const setEffort = (next: Effort) => { setInnerEffort(next); onEffortChange?.(next); };
return ( <div {...stylex.props(style)}> <Menu.Root open={menuOpen} onOpenChange={(open) => { setMenuOpen(open); if (!open) { setModelOpen(false); setQuery(""); } }} modal={false} disabled={disabled} > <Menu.Trigger render={<Button disabled={disabled} />}> {selected?.label ?? "Select model"} </Menu.Trigger> <Menu.Portal> <Menu.Positioner sideOffset={8}> <Menu.Popup style={styles.menu}> <Menu.Item closeOnClick={false} onClick={() => setFast(!fastOn)} > <Menu.ItemText>Fast</Menu.ItemText> <span {...stylex.props(styles.switchHit)} onClick={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()} > <Switch.Root variant="solid" size="sm" palette={accentPalette}> <Switch.Control checked={fastOn} onCheckedChange={setFast} aria-label="Fast" /> </Switch.Root> </span> </Menu.Item>
<Menu.SubmenuRoot> <Menu.SubmenuTrigger> <Menu.ItemText>Effort</Menu.ItemText> <Menu.ItemCommand>{effortLabel}</Menu.ItemCommand> <ChevronRightIcon /> </Menu.SubmenuTrigger> <Menu.Portal> <Menu.Positioner side="right" align="start" sideOffset={8}> <Menu.Popup> <Menu.RadioGroup value={effortValue} onValueChange={(value) => setEffort(value as Effort)} > {efforts.map((item) => ( <Menu.RadioItem key={item.value} value={item.value} closeOnClick={false} > <Menu.ItemText>{item.label}</Menu.ItemText> <Menu.RadioItemIndicator /> </Menu.RadioItem> ))} </Menu.RadioGroup> </Menu.Popup> </Menu.Positioner> </Menu.Portal> </Menu.SubmenuRoot>
<Menu.SubmenuRoot defaultOpen={defaultModelOpen} onOpenChange={(open) => { setModelOpen(open); if (open) setQuery(""); }} > <Menu.SubmenuTrigger> <Menu.ItemText>Model</Menu.ItemText> <Menu.ItemCommand>{selected?.label}</Menu.ItemCommand> <ChevronRightIcon /> </Menu.SubmenuTrigger> <Menu.Portal> <Menu.Positioner side="right" align="start" sideOffset={8}> <Menu.Popup style={styles.modelMenu}> <Combobox.Root items={items} itemToStringLabel={(model: Model | null) => model?.label ?? ""} itemToStringValue={(model: Model | null) => model?.value ?? ""} filter={(model: Model, query: string) => { const q = query.toLowerCase(); return ( model.label.toLowerCase().includes(q) || model.value.toLowerCase().includes(q) || modelTags.some((tag) => tag.toLowerCase().includes(q)) ); }} defaultValue={selectedModel} value={value !== undefined ? (items.find((m) => m.value === value) ?? undefined) : undefined} inputValue={query} onInputValueChange={(next, details) => { if (details.reason === "item-press") return; setQuery(next); }} onValueChange={(model: Model | null) => { if (model) setModel(model.value); setQuery(""); }} open disabled={disabled} style={styles.comboRoot} > <Combobox.Search placeholder={searchPlaceholder} style={styles.search} /> <Combobox.PanelList> <Combobox.Empty>No models found</Combobox.Empty> <Combobox.List> {(model: Model) => ( <Combobox.Item key={model.value} value={model}> <span {...stylex.props(styles.labelRow)}> <Combobox.ItemText style={styles.itemText}> {model.label} </Combobox.ItemText> {modelTags.length > 0 ? ( <span {...stylex.props(styles.tags)}> {modelTags.map((tag) => ( <span key={tag}>{tag}</span> ))} </span> ) : null} </span> <Combobox.ItemIndicator /> </Combobox.Item> )} </Combobox.List> </Combobox.PanelList> </Combobox.Root> <Menu.Separator /> <Menu.Item onClick={onAddModels}> <Menu.ItemText>{addLabel}</Menu.ItemText> </Menu.Item> </Menu.Popup> </Menu.Positioner> </Menu.Portal> </Menu.SubmenuRoot> </Menu.Popup> </Menu.Positioner> </Menu.Portal> {name ? <input type="hidden" name={name} value={selectedId} /> : null} </Menu.Root> </div> );}