import { type ProviderKind, PROVIDER_DISPLAY_NAMES, type ResolvedKeybindingsConfig, type ServerProvider, } from "@t3tools/contracts"; import { resolveSelectableModel } from "@t3tools/shared/model"; import { memo, useMemo, useState, useCallback, useEffect, useLayoutEffect, useRef } from "react"; import { SearchIcon } from "lucide-react"; import { ModelListRow } from "./ModelListRow"; import { ModelPickerSidebar } from "./ModelPickerSidebar"; import { isModelPickerNewModel } from "./modelPickerModelHighlights"; import { buildModelPickerSearchText, scoreModelPickerSearch } from "./modelPickerSearch"; import { Combobox, ComboboxEmpty, ComboboxInput, ComboboxList } from "../ui/combobox"; import { ModelEsque, PROVIDER_ICON_BY_PROVIDER } from "./providerIconUtils"; import { modelPickerJumpCommandForIndex, modelPickerJumpIndexFromCommand, resolveShortcutCommand, shortcutLabelForCommand, } from "../../keybindings"; import { useSettings, useUpdateSettings } from "~/hooks/useSettings"; import { cn } from "~/lib/utils"; import { TooltipProvider } from "../ui/tooltip"; type ModelPickerItem = { slug: string; name: string; shortName?: string; subProvider?: string; provider: ProviderKind; }; const EMPTY_MODEL_JUMP_LABELS = new Map(); export const ModelPickerContent = memo(function ModelPickerContent(props: { provider: ProviderKind; model: string; lockedProvider: ProviderKind | null; providers?: ReadonlyArray; keybindings?: ResolvedKeybindingsConfig; modelOptionsByProvider: Record>; terminalOpen: boolean; onRequestClose?: () => void; onProviderModelChange: (provider: ProviderKind, model: string) => void; }) { const { keybindings: providedKeybindings, modelOptionsByProvider, onProviderModelChange } = props; const [searchQuery, setSearchQuery] = useState(""); const searchInputRef = useRef(null); const listRegionRef = useRef(null); const highlightedModelKeyRef = useRef(null); const favorites = useSettings((s) => s.favorites ?? []); const [selectedProvider, setSelectedProvider] = useState(() => { if (props.lockedProvider !== null) { return props.lockedProvider; } return favorites.length > 0 ? "favorites" : props.provider; }); const keybindings = useMemo( () => providedKeybindings ?? [], [providedKeybindings], ); const { updateSettings } = useUpdateSettings(); const focusSearchInput = useCallback(() => { searchInputRef.current?.focus({ preventScroll: true }); }, []); const handleSelectProvider = useCallback( (provider: ProviderKind | "favorites") => { setSelectedProvider(provider); window.requestAnimationFrame(() => { focusSearchInput(); }); }, [focusSearchInput], ); useLayoutEffect(() => { focusSearchInput(); const frame = window.requestAnimationFrame(() => { focusSearchInput(); }); const timeout = window.setTimeout(() => { focusSearchInput(); }, 0); return () => { window.cancelAnimationFrame(frame); window.clearTimeout(timeout); }; }, [focusSearchInput]); // Create a Set for efficient lookup const favoritesSet = useMemo(() => { return new Set(favorites.map((fav) => `${fav.provider}:${fav.model}`)); }, [favorites]); const favoriteOrder = useMemo(() => { return new Map( favorites.map((favorite, index) => [`${favorite.provider}:${favorite.model}`, index]), ); }, [favorites]); const readyProviderSet = useMemo(() => { if (!props.providers || props.providers.length === 0) { return null; } return new Set( props.providers .filter((provider) => provider.status === "ready") .map((provider) => provider.provider), ); }, [props.providers]); // Flatten models into a searchable array const flatModels = useMemo(() => { return Object.entries(props.modelOptionsByProvider).flatMap(([providerKind, models]) => { if (readyProviderSet && !readyProviderSet.has(providerKind as ProviderKind)) { return []; } return models.map((m) => ({ slug: m.slug, name: m.name, ...(m.shortName ? { shortName: m.shortName } : {}), ...(m.subProvider ? { subProvider: m.subProvider } : {}), provider: providerKind as ProviderKind, })) satisfies Array; }); }, [props.modelOptionsByProvider, readyProviderSet]); // Filter models based on search query and selected provider const filteredModels = useMemo(() => { let result = flatModels; // Apply tokenized fuzzy search across the combined provider/model search fields. if (searchQuery.trim()) { const rankedMatches = result .map((model) => ({ model, score: scoreModelPickerSearch( { ...model, isFavorite: favoritesSet.has(`${model.provider}:${model.slug}`), }, searchQuery, ), isFavorite: favoritesSet.has(`${model.provider}:${model.slug}`), tieBreaker: buildModelPickerSearchText(model), })) .filter( ( rankedModel, ): rankedModel is { model: ModelPickerItem; score: number; isFavorite: boolean; tieBreaker: string; } => rankedModel.score !== null, ); // When searching, we only respect locked provider, ignoring sidebar selection if (props.lockedProvider !== null) { return rankedMatches .filter((rankedModel) => rankedModel.model.provider === props.lockedProvider) .toSorted((a, b) => { const scoreDelta = a.score - b.score; if (scoreDelta !== 0) { return scoreDelta; } if (a.isFavorite !== b.isFavorite) { return a.isFavorite ? -1 : 1; } return a.tieBreaker.localeCompare(b.tieBreaker); }) .map((rankedModel) => rankedModel.model); } return rankedMatches .toSorted((a, b) => { const scoreDelta = a.score - b.score; if (scoreDelta !== 0) { return scoreDelta; } if (a.isFavorite !== b.isFavorite) { return a.isFavorite ? -1 : 1; } return a.tieBreaker.localeCompare(b.tieBreaker); }) .map((rankedModel) => rankedModel.model); } // Locked provider mode always shows that provider's models, with favorites first. if (props.lockedProvider !== null) { result = result.filter((m) => m.provider === props.lockedProvider); } else if (selectedProvider === "favorites") { result = result.filter((m) => favoritesSet.has(`${m.provider}:${m.slug}`)); } else { result = result.filter((m) => m.provider === selectedProvider); } return result.toSorted((a, b) => { const aOrder = favoriteOrder.get(`${a.provider}:${a.slug}`); const bOrder = favoriteOrder.get(`${b.provider}:${b.slug}`); if (aOrder !== undefined && bOrder !== undefined) { return aOrder - bOrder; } if (aOrder !== undefined) { return -1; } if (bOrder !== undefined) { return 1; } return 0; }); }, [ favoriteOrder, favoritesSet, flatModels, props.lockedProvider, searchQuery, selectedProvider, ]); const handleModelSelect = useCallback( (modelSlug: string, provider: ProviderKind) => { const resolvedModel = resolveSelectableModel( provider, modelSlug, modelOptionsByProvider[provider], ); if (resolvedModel) { onProviderModelChange(provider, resolvedModel); } }, [modelOptionsByProvider, onProviderModelChange], ); const toggleFavorite = useCallback( (provider: ProviderKind, model: string) => { const newFavorites = [...favorites]; const index = newFavorites.findIndex((f) => f.provider === provider && f.model === model); if (index >= 0) { newFavorites.splice(index, 1); } else { newFavorites.push({ provider, model }); } updateSettings({ favorites: newFavorites }); }, [favorites, updateSettings], ); const isLocked = props.lockedProvider !== null; const isSearching = searchQuery.trim().length > 0; const showSidebar = !isLocked && !isSearching; const LockedProviderIcon = isLocked && props.lockedProvider ? PROVIDER_ICON_BY_PROVIDER[props.lockedProvider] : null; const modelJumpCommandByKey = useMemo(() => { const mapping = new Map< string, NonNullable> >(); for (const [visibleModelIndex, model] of filteredModels.entries()) { const jumpCommand = modelPickerJumpCommandForIndex(visibleModelIndex); if (!jumpCommand) { return mapping; } mapping.set(`${model.provider}:${model.slug}`, jumpCommand); } return mapping; }, [filteredModels]); const modelJumpModelKeys = useMemo( () => [...modelJumpCommandByKey.keys()], [modelJumpCommandByKey], ); const allModelKeys = useMemo( (): string[] => flatModels.map((model) => `${model.provider}:${model.slug}`), [flatModels], ); const filteredModelKeys = useMemo( (): string[] => filteredModels.map((model) => `${model.provider}:${model.slug}`), [filteredModels], ); const filteredModelByKey = useMemo( (): ReadonlyMap => new Map(filteredModels.map((model) => [`${model.provider}:${model.slug}`, model] as const)), [filteredModels], ); const modelJumpShortcutContext = useMemo( () => ({ terminalFocus: false, terminalOpen: props.terminalOpen, modelPickerOpen: true, }) as const, [props.terminalOpen], ); const modelJumpLabelByKey = useMemo((): ReadonlyMap => { if (modelJumpCommandByKey.size === 0) { return EMPTY_MODEL_JUMP_LABELS; } const shortcutLabelOptions = { platform: navigator.platform, context: modelJumpShortcutContext, }; const mapping = new Map(); for (const [modelKey, command] of modelJumpCommandByKey) { const label = shortcutLabelForCommand(keybindings, command, shortcutLabelOptions); if (label) { mapping.set(modelKey, label); } } return mapping.size > 0 ? mapping : EMPTY_MODEL_JUMP_LABELS; }, [keybindings, modelJumpCommandByKey, modelJumpShortcutContext]); useEffect(() => { const onWindowKeyDown = (event: globalThis.KeyboardEvent) => { if (event.defaultPrevented || event.repeat) { return; } const command = resolveShortcutCommand(event, keybindings, { platform: navigator.platform, context: modelJumpShortcutContext, }); const jumpIndex = modelPickerJumpIndexFromCommand(command ?? ""); if (jumpIndex === null) { return; } const targetModelKey = modelJumpModelKeys[jumpIndex]; if (!targetModelKey) { return; } const [provider, slug] = targetModelKey.split(":") as [ProviderKind, string]; event.preventDefault(); event.stopPropagation(); handleModelSelect(slug, provider); }; window.addEventListener("keydown", onWindowKeyDown, true); return () => { window.removeEventListener("keydown", onWindowKeyDown, true); }; }, [handleModelSelect, keybindings, modelJumpModelKeys, modelJumpShortcutContext]); useLayoutEffect(() => { const listRegion = listRegionRef.current; if (!listRegion) { return; } let cancelled = false; let frame = 0; let nestedFrame = 0; let timeout = 0; const measureScrollArea = () => { if (cancelled) { return; } const viewport = listRegion.querySelector('[data-slot="scroll-area-viewport"]'); if (!viewport || viewport.scrollHeight <= viewport.clientHeight) { return; } const originalScrollTop = viewport.scrollTop; const maxScrollTop = viewport.scrollHeight - viewport.clientHeight; if (maxScrollTop <= 0) { return; } viewport.scrollTop = Math.min(originalScrollTop + 1, maxScrollTop); viewport.scrollTop = originalScrollTop; }; queueMicrotask(measureScrollArea); frame = window.requestAnimationFrame(() => { measureScrollArea(); nestedFrame = window.requestAnimationFrame(measureScrollArea); }); timeout = window.setTimeout(measureScrollArea, 0); return () => { cancelled = true; window.cancelAnimationFrame(frame); window.cancelAnimationFrame(nestedFrame); window.clearTimeout(timeout); }; }, [filteredModelKeys]); return (
{/* Locked provider header (only shown in locked mode) */} {isLocked && LockedProviderIcon && props.lockedProvider && (
{PROVIDER_DISPLAY_NAMES[props.lockedProvider]}
)} {/* Sidebar (only in unlocked mode) */} {showSidebar && ( )} {/* Main content area */} { highlightedModelKeyRef.current = typeof modelKey === "string" ? modelKey : null; }} onValueChange={(modelKey) => { if (typeof modelKey !== "string") { return; } const [provider, slug] = modelKey.split(":") as [ProviderKind, string]; handleModelSelect(slug, provider); }} >
{/* Search bar */}
} value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} onKeyDown={(e) => { if (e.key === "Escape") { e.preventDefault(); e.stopPropagation(); props.onRequestClose?.(); return; } if (e.key === "Enter" && highlightedModelKeyRef.current) { ( e as typeof e & { preventBaseUIHandler?: () => void } ).preventBaseUIHandler?.(); e.preventDefault(); e.stopPropagation(); const [provider, slug] = highlightedModelKeyRef.current.split(":") as [ ProviderKind, string, ]; handleModelSelect(slug, provider); return; } e.stopPropagation(); }} onMouseDown={(e) => e.stopPropagation()} onTouchStart={(e) => e.stopPropagation()} size="sm" />
{/* Model list */}
{filteredModelKeys.map((modelKey, index) => { const model = filteredModelByKey.get(modelKey); if (!model) { return null; } return ( toggleFavorite(model.provider, model.slug)} /> ); })}
No models found
); });