"use client"; import { Check, ChevronDown, ChevronLeft, ChevronRight, RotateCcw, } from "lucide-react"; import { AnimatePresence, animate, motion, useMotionTemplate, useMotionValue, useReducedMotion, useTransform, } from "motion/react"; import { type KeyboardEvent, type PointerEvent as ReactPointerEvent, type ReactNode, type RefObject, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { createPortal } from "react-dom"; import { EASE_OUT, SPRING_GLIDE, SPRING_LAYOUT, SPRING_PANEL, SPRING_PRESS, SPRING_SWAP, } from "@/lib/ease"; import { useOnOpen } from "@/lib/hooks/use-on-open"; import { useSlider } from "@/lib/hooks/use-slider"; import { PresenceGate } from "@/lib/presence-gate"; import { capturePointer, releasePointer, TOUCH_GESTURE_CLASS } from "@/lib/touch"; import { cn } from "@/lib/utils"; type Side = "top" | "bottom"; type Align = "start" | "end"; type PanelView = "effort" | "models"; const VIEWPORT_PADDING = 12; const PANEL_WIDTH = 272; const PANEL_MAX_HEIGHT = 320; export interface ModelSelectorEffortOption { value: string; label: string; description?: string; disabled?: boolean; } export const DEFAULT_MODEL_EFFORTS: readonly ModelSelectorEffortOption[] = [ { value: "low", label: "Low" }, { value: "medium", label: "Medium" }, { value: "high", label: "High" }, { value: "xhigh", label: "X-high" }, { value: "max", label: "Max" }, { value: "ultra", label: "Ultra" }, ]; export interface ModelSelectorOption { value: string; label: ReactNode; /** Accessible text when `label` is not a string. Falls back to `value`. */ textValue?: string; description?: ReactNode; icon?: ReactNode; badge?: ReactNode; disabled?: boolean; /** Model-specific effort choices. Falls back to the root `efforts` prop. */ efforts?: readonly ModelSelectorEffortOption[]; defaultEffort?: string; } export interface ModelSelectorProps { models: readonly ModelSelectorOption[]; value?: string; defaultValue?: string; onValueChange?: (value: string) => void; efforts?: readonly ModelSelectorEffortOption[]; effort?: string; defaultEffort?: string; onEffortChange?: (effort: string) => void; open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean) => void; disabled?: boolean; placeholder?: ReactNode; /** Accessible name for the trigger and effort panel. */ label?: string; modelListLabel?: string; effortLabel?: string; side?: Side; align?: Align; sideOffset?: number; className?: string; triggerClassName?: string; contentClassName?: string; } function optionText(option: ModelSelectorOption | undefined) { if (!option) return undefined; if (option.textValue) return option.textValue; if (typeof option.label === "string" || typeof option.label === "number") { return String(option.label); } return option.value; } function enabledModelValue( models: readonly ModelSelectorOption[], candidate: string | null | undefined, ) { return candidate !== null && candidate !== undefined ? models.find((model) => model.value === candidate && !model.disabled)?.value ?? null : null; } function enabledEffortValue( efforts: readonly ModelSelectorEffortOption[], candidate: string | null | undefined, ) { return candidate !== null && candidate !== undefined ? efforts.find((effort) => effort.value === candidate && !effort.disabled) ?.value ?? null : null; } function effortsFor( model: ModelSelectorOption | undefined, fallback: readonly ModelSelectorEffortOption[], ) { return model?.efforts ?? fallback; } type TriggerRect = { left: number; top: number; width: number; height: number; viewportWidth: number; viewportHeight: number; }; function sameTriggerRect(current: TriggerRect | null, next: TriggerRect) { return ( current?.left === next.left && current.top === next.top && current.width === next.width && current.height === next.height && current.viewportWidth === next.viewportWidth && current.viewportHeight === next.viewportHeight ); } /** Measure only the anchor. Panel size changes must never feed positioning. */ function useTriggerRect( triggerRef: RefObject, active: boolean, ) { const [rect, setRect] = useState(null); const update = useCallback(() => { const trigger = triggerRef.current; if (!trigger) return; const box = trigger.getBoundingClientRect(); const next = { left: box.left, top: box.top, width: box.width, height: box.height, viewportWidth: window.innerWidth, viewportHeight: window.innerHeight, }; setRect((current) => (sameTriggerRect(current, next) ? current : next)); }, [triggerRef]); useLayoutEffect(() => { update(); if (!active) return; const trigger = triggerRef.current; const observer = new ResizeObserver(update); if (trigger) observer.observe(trigger); window.addEventListener("scroll", update, true); window.addEventListener("resize", update); return () => { observer.disconnect(); window.removeEventListener("scroll", update, true); window.removeEventListener("resize", update); }; }, [active, triggerRef, update]); return rect; } /** Keep the active model valid while a streamed catalog changes identity. */ function useModelCursor({ open, models, value, }: { open: boolean; models: readonly ModelSelectorOption[]; value: string | undefined; }) { const [cursor, setCursor] = useState(null); const enabled = useMemo( () => models.filter((model) => !model.disabled), [models], ); const liveCursor = enabledModelValue(enabled, cursor); if (cursor !== null && liveCursor === null) setCursor(null); const fallback = enabledModelValue(enabled, value) ?? enabled[0]?.value ?? null; const activeValue = liveCursor ?? fallback; const latest = useRef({ open, enabled, value }); useLayoutEffect(() => { latest.current = { open, enabled, value }; }); const reset = useCallback(() => setCursor(null), []); const moveTo = useCallback( (next: string | null) => setCursor((current) => (current === next ? current : next)), [], ); const move = useCallback((direction: 1 | -1 | "first" | "last") => { const current = latest.current; if (!current.open || current.enabled.length === 0) return; setCursor((previous) => { const rows = current.enabled; const last = rows.length - 1; const from = enabledModelValue(rows, previous) ?? enabledModelValue(rows, current.value) ?? rows[0]?.value ?? null; const at = Math.max( rows.findIndex((model) => model.value === from), 0, ); const index = direction === "first" ? 0 : direction === "last" ? last : (at + direction + rows.length) % rows.length; return rows[index]?.value ?? null; }); }, []); useOnOpen(open, reset); return { activeValue, move, moveTo }; } function EffortSlider({ efforts, value, onValueChange, label, disabled, reduce, }: { efforts: readonly ModelSelectorEffortOption[]; value: string | undefined; onValueChange: (value: string) => void; label: string; disabled: boolean; reduce: boolean; }) { const [hovered, setHovered] = useState(false); const [focused, setFocused] = useState(false); const [dragging, setDragging] = useState(false); const activePointer = useRef(null); const animation = useRef<{ stop: () => void } | null>(null); const enabled = efforts.filter((effort) => !effort.disabled); const resolved = enabled.findIndex((effort) => effort.value === value) >= 0 ? (value as string) : enabled[0]?.value; const index = Math.max( enabled.findIndex((effort) => effort.value === resolved), 0, ); const lastEmitted = useRef(value); useLayoutEffect(() => { lastEmitted.current = value; }, [value]); const { percent, current, commit, sliderProps } = useSlider({ value: index, min: 0, max: Math.max(enabled.length - 1, 0), step: 1, disabled: disabled || enabled.length < 2, "aria-label": label, formatValueText: (numeric) => enabled[Math.round(numeric)]?.label ?? "", onValueChange: (numeric) => { const next = enabled[Math.round(numeric)]; if (next && next.value !== lastEmitted.current) { lastEmitted.current = next.value; onValueChange(next.value); } }, }); const position = useMotionValue(percent); useLayoutEffect(() => { if (dragging || activePointer.current !== null) return; if (reduce) { position.set(percent); return; } animation.current = animate(position, percent, { type: "spring", ...SPRING_GLIDE, }); return () => animation.current?.stop(); }, [dragging, percent, position, reduce]); const thumbX = useMotionTemplate`${position}%`; const fillScale = useTransform(position, (progress) => progress / 100); const currentOption = enabled[Math.round(current)]; const denominator = Math.max(enabled.length - 1, 1); const movePointer = (event: ReactPointerEvent) => { const track = event.currentTarget; const rect = track.getBoundingClientRect(); if (rect.width <= 0) return; // Pointer coordinates and the visual thumb share the same inset endpoints, // including while the opening panel is still scaled. const inset = 14 * (track.offsetWidth ? rect.width / track.offsetWidth : 1); const travel = Math.max(rect.width - inset * 2, 1); const progress = Math.min(1, Math.max(0, (event.clientX - rect.left - inset) / travel)); position.set(progress * 100); commit(progress * Math.max(enabled.length - 1, 0)); }; const endPointer = (event: ReactPointerEvent) => { if (event.pointerId !== activePointer.current) return; activePointer.current = null; setDragging(false); releasePointer(event.currentTarget, event.pointerId); }; if (enabled.length === 0) { return (

Effort is managed automatically for this model.

); } return (
{ if (disabled || enabled.length < 2 || event.button !== 0 || activePointer.current !== null) return; event.preventDefault(); animation.current?.stop(); activePointer.current = event.pointerId; setDragging(true); capturePointer(event.currentTarget, event.pointerId); event.currentTarget.querySelector('[role="slider"]')?.focus({ preventScroll: true }); movePointer(event); }} onPointerMove={(event) => { if (!disabled && event.pointerId === activePointer.current) movePointer(event); }} onPointerUp={(event) => { if (!disabled && event.pointerId === activePointer.current) movePointer(event); endPointer(event); }} onPointerCancel={endPointer} onLostPointerCapture={endPointer} className={cn( "relative mt-3 h-8 w-full touch-none", TOUCH_GESTURE_CLASS, disabled || enabled.length < 2 ? "pointer-events-none opacity-50" : "cursor-grab active:cursor-grabbing", )} onPointerEnter={() => setHovered(true)} onPointerLeave={() => setHovered(false)} >
{enabled.map((effort, effortIndex) => { const tickLeft = enabled.length === 1 ? "0%" : `${(effortIndex / denominator) * 100}%`; return ( ); })}
setFocused(true)} onBlur={() => setFocused(false)} > {(hovered || focused || dragging) && currentOption?.description ? ( {currentOption.description} ) : null}
); } export function ModelSelector({ models, value, defaultValue, onValueChange, efforts = DEFAULT_MODEL_EFFORTS, effort, defaultEffort = "high", onEffortChange, open: openProp, defaultOpen = false, onOpenChange, disabled = false, placeholder = "Select model", label = "Model and effort", modelListLabel = "Select model", effortLabel = "Reasoning effort", side = "top", align = "end", sideOffset = 8, className, triggerClassName, contentClassName, }: ModelSelectorProps) { const reduce = useReducedMotion() ?? false; const baseId = useId(); const triggerId = `${baseId}-trigger`; const panelId = `${baseId}-panel`; const listId = `${baseId}-list`; const triggerRef = useRef(null); const anchorRef = useRef(null); const panelRef = useRef(null); const modelButtonRef = useRef(null); const focusActiveModel = useRef(false); const focusModelButton = useRef(false); const effortByModel = useRef(new Map()); const [portalReady, setPortalReady] = useState(false); const [view, setView] = useState("effort"); const [internalValue, setInternalValue] = useState(defaultValue); const [internalEffort, setInternalEffort] = useState(defaultEffort); const [internalOpen, setInternalOpen] = useState(defaultOpen); const controlledValue = value !== undefined; const controlledEffort = effort !== undefined; const controlledOpen = openProp !== undefined; const currentValue = controlledValue ? value : internalValue; const rawEffort = controlledEffort ? effort : internalEffort; const open = controlledOpen ? openProp : internalOpen; const currentModel = models.find((model) => model.value === currentValue); const currentText = optionText(currentModel); const currentEfforts = useMemo( () => effortsFor(currentModel, efforts), [currentModel, efforts], ); const currentEffortValue = enabledEffortValue(currentEfforts, rawEffort) ?? enabledEffortValue(currentEfforts, currentModel?.defaultEffort) ?? enabledEffortValue(currentEfforts, defaultEffort) ?? currentEfforts.find((option) => !option.disabled)?.value; const currentEffort = currentEfforts.find( (option) => option.value === currentEffortValue, ); const resetEffortValue = enabledEffortValue(currentEfforts, currentModel?.defaultEffort) ?? enabledEffortValue(currentEfforts, defaultEffort) ?? currentEfforts.find((option) => !option.disabled)?.value; useLayoutEffect(() => { if (currentValue && currentEffortValue) { effortByModel.current.set(currentValue, currentEffortValue); } }, [currentEffortValue, currentValue]); const setOpen = useCallback( (next: boolean) => { if (!controlledOpen) setInternalOpen(next); onOpenChange?.(next); }, [controlledOpen, onOpenChange], ); const commitEffort = useCallback( (next: string) => { if (currentValue) effortByModel.current.set(currentValue, next); if (!controlledEffort) setInternalEffort(next); onEffortChange?.(next); }, [controlledEffort, currentValue, onEffortChange], ); const { activeValue, move, moveTo } = useModelCursor({ open, models, value: currentValue, }); const activeIndex = models.findIndex( (model) => model.value === activeValue, ); const resetView = useCallback(() => setView("effort"), []); useOnOpen(open, resetView); const showModels = useCallback(() => { focusActiveModel.current = true; setView("models"); }, []); const showEffort = useCallback(() => { focusModelButton.current = true; setView("effort"); }, []); const choose = useCallback( (next: string) => { const model = models.find((option) => option.value === next); if (!model || model.disabled) return; const nextEfforts = effortsFor(model, efforts); const nextEffort = enabledEffortValue(nextEfforts, effortByModel.current.get(next)) ?? enabledEffortValue(nextEfforts, currentEffortValue) ?? enabledEffortValue(nextEfforts, model.defaultEffort) ?? enabledEffortValue(nextEfforts, defaultEffort) ?? nextEfforts.find((option) => !option.disabled)?.value; if (!controlledValue) setInternalValue(next); onValueChange?.(next); if (nextEffort && nextEffort !== currentEffortValue) { if (!controlledEffort) setInternalEffort(nextEffort); onEffortChange?.(nextEffort); } if (nextEffort) effortByModel.current.set(next, nextEffort); focusModelButton.current = true; setView("effort"); }, [ controlledEffort, controlledValue, currentEffortValue, defaultEffort, efforts, models, onEffortChange, onValueChange, ], ); useEffect(() => setPortalReady(true), []); useLayoutEffect(() => { if (view !== "models" || !focusActiveModel.current || activeIndex < 0) { return; } focusActiveModel.current = false; document.getElementById(`${baseId}-option-${activeIndex}`)?.focus(); }, [activeIndex, baseId, view]); useLayoutEffect(() => { if (!open || view !== "effort" || !focusModelButton.current) return; focusModelButton.current = false; modelButtonRef.current?.focus(); }, [open, view]); useEffect(() => { if (!open) return; const handlePointerDown = (event: PointerEvent) => { const target = event.target as Node; if ( !triggerRef.current?.contains(target) && !panelRef.current?.contains(target) ) { setOpen(false); } }; const handleFocus = (event: FocusEvent) => { const target = event.target as Node; if ( !triggerRef.current?.contains(target) && !panelRef.current?.contains(target) ) { setOpen(false); } }; const handleEscape = (event: globalThis.KeyboardEvent) => { if (event.key !== "Escape") return; event.preventDefault(); if (view === "models") showEffort(); else { setOpen(false); triggerRef.current?.focus(); } }; window.addEventListener("pointerdown", handlePointerDown); window.addEventListener("focusin", handleFocus); window.addEventListener("keydown", handleEscape); return () => { window.removeEventListener("pointerdown", handlePointerDown); window.removeEventListener("focusin", handleFocus); window.removeEventListener("keydown", handleEscape); }; }, [open, setOpen, showEffort, view]); // The button scales during a press; its stationary wrapper is the anchor. const triggerRect = useTriggerRect(anchorRef, portalReady && open); const panelWidth = triggerRect ? Math.min(PANEL_WIDTH, window.innerWidth - VIEWPORT_PADDING * 2) : PANEL_WIDTH; let placement = side; if (triggerRect) { const above = triggerRect.top - sideOffset - VIEWPORT_PADDING; const below = window.innerHeight - triggerRect.top - triggerRect.height - sideOffset - VIEWPORT_PADDING; if (side === "top" && above < PANEL_MAX_HEIGHT && below > above) { placement = "bottom"; } else if ( side === "bottom" && below < PANEL_MAX_HEIGHT && above > below ) { placement = "top"; } } const panelLeft = triggerRect ? Math.min( Math.max( VIEWPORT_PADDING, align === "end" ? triggerRect.left + triggerRect.width - panelWidth : triggerRect.left, ), Math.max( VIEWPORT_PADDING, window.innerWidth - panelWidth - VIEWPORT_PADDING, ), ) : 0; const panelAnchorTop = triggerRect ? placement === "top" ? triggerRect.top - sideOffset : triggerRect.top + triggerRect.height + sideOffset : 0; const availablePanelHeight = triggerRect ? placement === "top" ? triggerRect.top - sideOffset - VIEWPORT_PADDING : window.innerHeight - triggerRect.top - triggerRect.height - sideOffset - VIEWPORT_PADDING : PANEL_MAX_HEIGHT; const handleTriggerKeyDown = (event: KeyboardEvent) => { if (event.key === "ArrowDown") { event.preventDefault(); focusModelButton.current = true; if (!open) setOpen(true); else { focusModelButton.current = false; modelButtonRef.current?.focus(); } return; } if (event.key === "Tab" && open && !event.shiftKey) { event.preventDefault(); modelButtonRef.current?.focus(); } }; const handleModelKeyDown = ( event: KeyboardEvent, selectedValue: string, ) => { if (event.key === "ArrowDown" || event.key === "ArrowUp") { event.preventDefault(); focusActiveModel.current = true; move(event.key === "ArrowDown" ? 1 : -1); return; } if (event.key === "Home" || event.key === "End") { event.preventDefault(); focusActiveModel.current = true; move(event.key === "Home" ? "first" : "last"); return; } if (event.key === "Enter" || event.key === " ") { event.preventDefault(); choose(selectedValue); } }; const effortView = ( {({ gate, isPresent }) => ( option.description) ? "pb-11" : "pb-3", )} >
)}
); const modelView = ( {({ gate, isPresent }) => ( {models.length === 0 ? (

No models available.

) : ( models.map((model, index) => { const selected = model.value === currentValue; const active = model.value === activeValue; const text = optionText(model); return ( { if (!model.disabled) moveTo(model.value); }} onPointerMove={() => { if (!model.disabled) moveTo(model.value); }} onKeyDown={(event) => handleModelKeyDown(event, model.value)} onClick={() => choose(model.value)} className="relative flex min-h-8 w-full items-center gap-2 rounded-xl px-2.5 py-1 text-left outline-none disabled:pointer-events-none disabled:opacity-40" > {active ? ( ); }) )}
)}
); const panel = portalReady ? createPortal( {open ? ( {({ gate }) => ( )} ) : null} , document.body, ) : null; return ( <>
{ const next = !open; setOpen(next); if (next && event.detail === 0) { focusModelButton.current = true; } }} onKeyDown={handleTriggerKeyDown} whileTap={reduce || disabled ? undefined : { scale: 0.97 }} transition={SPRING_SWAP} className={cn( "group flex h-8 min-w-40 max-w-56 items-center gap-1.5 rounded-xl px-2 text-xs font-medium text-muted-foreground outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", open && "bg-muted text-foreground", triggerClassName, )} >
{panel} ); }