import React from 'react'; import { DndContext, MouseSensor, PointerSensor, closestCenter, useSensor, useSensors, type DragEndEvent, } from '@dnd-kit/core'; import { SortableContext, arrayMove, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { CSS as DndCSS } from '@dnd-kit/utilities'; import { Icon } from '@/components/icon/Icon'; import { Input } from '@/components/ui/input'; import { matchesRankQuery } from '@/lib/search/fuzzySearch'; import { ProviderLogo } from '@/components/ui/ProviderLogo'; import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { getCurrentIntlLocale } from '@/lib/i18n'; import { mergeModelMetadataWithLiveModel } from '@/lib/modelMetadata'; import { getModelDisplayName as getSharedModelDisplayName } from '@/lib/modelDisplay'; import { cn } from '@/lib/utils'; import { useModelPickerSectionsStore } from '@/stores/useModelPickerSectionsStore'; import type { ModelMetadata } from '@/types'; type ProviderModel = Record & { id?: string; name?: string }; export type ModelPickerProvider = { id: string; name?: string; models?: ProviderModel[]; }; export type ModelPickerEntry = { model: ProviderModel; providerID: string; modelID: string; }; type ModelPickerFavoriteEntry = ModelPickerEntry; type HiddenModel = { providerID: string; modelID: string }; type IndexSelectionStore = { getSnapshot: () => number; subscribe: (listener: () => void) => () => void; subscribeIndex: (index: number, listener: () => void) => () => void; set: (value: number) => void; }; const formatCompactNumber = (value: number) => new Intl.NumberFormat(getCurrentIntlLocale(), { notation: 'compact', compactDisplay: 'short', maximumFractionDigits: 1, minimumFractionDigits: 0, }).format(value); const formatUsdCurrency = (value: number) => new Intl.NumberFormat(getCurrentIntlLocale(), { style: 'currency', currency: 'USD', maximumFractionDigits: 4, minimumFractionDigits: 2, }).format(value); const getModelDisplayName = (model: Record) => { return getSharedModelDisplayName(model, undefined, { maxLength: 40 }); }; const formatModelContextTokens = (value?: number | null) => { if (typeof value !== 'number' || Number.isNaN(value)) return ''; if (value === 0) return '0'; const formatted = formatCompactNumber(value); return formatted.endsWith('.0') ? formatted.slice(0, -2) : formatted; }; const formatCost = (value?: number | null) => { if (typeof value !== 'number' || !Number.isFinite(value)) return '—'; return formatUsdCurrency(value); }; const hasTooltipMetadata = (metadata?: ModelMetadata) => { if (!metadata) return false; return Boolean( metadata.tool_call || metadata.reasoning || metadata.cost?.input !== undefined || metadata.cost?.output !== undefined || (metadata.modalities?.input?.length ?? 0) > 0 || (metadata.modalities?.output?.length ?? 0) > 0, ); }; const ModelPickerRowTooltip: React.FC<{ metadata?: ModelMetadata; active: boolean; labels: ModelPickerListProps['labels']; children: React.ReactElement; }> = ({ metadata, active, labels, children }) => { const [delayedActive, setDelayedActive] = React.useState(false); React.useEffect(() => { if (!active) { setDelayedActive(false); return; } const timeout = window.setTimeout(() => setDelayedActive(true), 450); return () => window.clearTimeout(timeout); }, [active]); if (!hasTooltipMetadata(metadata)) return children; const inputModalities = metadata?.modalities?.input ?? []; const outputModalities = metadata?.modalities?.output ?? []; const capabilities = [ metadata?.tool_call ? labels.capabilityToolCalling : null, metadata?.reasoning ? labels.capabilityReasoning : null, ].filter(Boolean); return ( {}}> {children} {active && delayedActive ? (
{capabilities.length > 0 ? (
{labels.capabilities} {capabilities.join(', ')}
) : null} {inputModalities.length > 0 ? (
{labels.input} {inputModalities.join(', ')}
) : null} {outputModalities.length > 0 ? (
{labels.output} {outputModalities.join(', ')}
) : null} {(metadata?.cost?.input !== undefined || metadata?.cost?.output !== undefined) ? (
{labels.costPerMillion} In {formatCost(metadata?.cost?.input)} · Out {formatCost(metadata?.cost?.output)}
) : null}
) : null}
); }; const createIndexSelectionStore = (): IndexSelectionStore => { let value = 0; const listeners = new Set<() => void>(); const listenersByIndex = new Map void>>(); const notify = (index: number) => { const listeners = listenersByIndex.get(index); if (!listeners) return; for (const listener of listeners) listener(); }; return { getSnapshot: () => value, subscribe: (listener) => { listeners.add(listener); return () => listeners.delete(listener); }, subscribeIndex: (index, listener) => { let listeners = listenersByIndex.get(index); if (!listeners) { listeners = new Set(); listenersByIndex.set(index, listeners); } listeners.add(listener); return () => { listeners.delete(listener); if (listeners.size === 0) listenersByIndex.delete(index); }; }, set: (nextValue) => { if (value === nextValue) return; const previousValue = value; value = nextValue; notify(previousValue); notify(nextValue); for (const listener of listeners) listener(); }, }; }; const ModelPickerRowHighlight: React.FC<{ store: IndexSelectionStore; index: number; renderVersion?: number; children: (isHighlighted: boolean) => React.ReactNode; }> = React.memo(({ store, index, children }) => { const [isHighlighted, setIsHighlighted] = React.useState(() => store.getSnapshot() === index); React.useEffect(() => { const sync = () => setIsHighlighted(store.getSnapshot() === index); sync(); return store.subscribeIndex(index, sync); }, [index, store]); return <>{children(isHighlighted)}; }); const ModelPickerFooter: React.FC<{ store: IndexSelectionStore; flatModelList: ModelPickerEntry[]; footerContent: ModelPickerListProps['footerContent']; fallback: React.ReactNode; }> = ({ store, flatModelList, footerContent, fallback }) => { const [selectedIndex, setSelectedIndex] = React.useState(() => store.getSnapshot()); React.useEffect(() => store.subscribe(() => setSelectedIndex(store.getSnapshot())), [store]); const activeEntry = flatModelList[selectedIndex]; return <>{typeof footerContent === 'function' ? footerContent(activeEntry) : (footerContent ?? fallback)}; }; type SortableFavoriteHandleProps = { attributes: ReturnType['attributes']; listeners: ReturnType['listeners']; setActivatorNodeRef: ReturnType['setActivatorNodeRef']; isDragging: boolean; }; const SortableFavoriteModelRow: React.FC<{ id: string; disabled?: boolean; children: (dragHandleProps: SortableFavoriteHandleProps) => React.ReactNode; }> = ({ id, disabled = false, children }) => { const { attributes, listeners, setNodeRef, setActivatorNodeRef, transform, transition, isDragging, } = useSortable({ id, disabled }); return (
{children({ attributes, listeners, setActivatorNodeRef, isDragging })}
); }; const SortableProviderSection: React.FC<{ id: string; disabled?: boolean; children: (dragHandleProps: SortableFavoriteHandleProps) => React.ReactNode; }> = ({ id, disabled = false, children }) => { const { attributes, listeners, setNodeRef, setActivatorNodeRef, transform, transition, isDragging, } = useSortable({ id, disabled }); return (
{children({ attributes, listeners, setActivatorNodeRef, isDragging })}
); }; const STICKY_HEADER_OFFSET = 32; const STICKY_FADE_MAX_SIZE = 52; const STICKY_FADE_MIN_SIZE = 36; const STICKY_FADE_CLEAR_MAX_SIZE = 28; const scrollIntoView = (container: HTMLElement | null, node: HTMLElement | null) => { if (!node) return; if (!container) { node.scrollIntoView({ block: 'nearest' }); return; } const containerRect = container.getBoundingClientRect(); const nodeRect = node.getBoundingClientRect(); const top = nodeRect.top - containerRect.top + container.scrollTop; const bottom = top + nodeRect.height; const viewTop = container.scrollTop; const viewBottom = viewTop + container.clientHeight; const viewTopWithHeader = viewTop + STICKY_HEADER_OFFSET; const target = top < viewTopWithHeader ? top - STICKY_HEADER_OFFSET : bottom > viewBottom ? bottom - container.clientHeight : viewTop; const max = Math.max(0, container.scrollHeight - container.clientHeight); container.scrollTop = Math.max(0, Math.min(target, max)); }; interface ModelPickerListProps { providers: ModelPickerProvider[]; favoriteModels: ModelPickerFavoriteEntry[]; recentModels: ModelPickerFavoriteEntry[]; modelsMetadata: Map; searchQuery: string; onSearchQueryChange: (value: string) => void; onSelect: (entry: ModelPickerEntry) => void; labels: { searchPlaceholder: string; noResults: string; favorites: string; recent: string; keyboardHint: string; notSelected?: string; favorite?: string; unfavorite?: string; capabilities?: string; capabilityToolCalling?: string; capabilityReasoning?: string; input?: string; output?: string; costPerMillion?: string; }; selectedModel?: { providerID: string; modelID: string } | null; hiddenModels?: HiddenModel[]; allowedProviderIds?: string[]; /** * Per-model gate, for callers whose feature needs a capability rather than a * provider (e.g. structured output). Applied on top of `allowedProviderIds`. */ isModelAllowed?: (providerID: string, modelID: string) => boolean; includeNotSelected?: boolean; onSelectNone?: () => void; selectionCount?: (entry: ModelPickerEntry) => number; disabled?: boolean; maxHeightClassName?: string; maxHeightStyle?: React.CSSProperties; sectionHeaderClassName?: string; rowClassName?: string; stickyHeaders?: boolean; autoFocus?: boolean; onEscape?: () => void; isFavorite?: (entry: ModelPickerEntry) => boolean; onToggleFavorite?: (entry: ModelPickerEntry) => void; renderRowEnd?: (entry: ModelPickerEntry, state: { isHighlighted: boolean; isSelected: boolean }) => React.ReactNode; onActiveKeyDown?: (event: React.KeyboardEvent, entry: ModelPickerEntry | undefined) => void; onActiveEntryChange?: (entry: ModelPickerEntry | undefined) => void; onVariantKey?: (event: React.KeyboardEvent, entry: ModelPickerEntry) => boolean; onReorderFavorite?: (active: ModelPickerEntry, over: ModelPickerEntry) => void; reorderFavoriteAriaLabel?: string; reorderFavoriteTitle?: string; providerOrder?: string[]; onReorderProvider?: (orderedProviderIDs: string[]) => void; reorderProviderTitle?: string; footerContent?: React.ReactNode | ((activeEntry: ModelPickerEntry | undefined) => React.ReactNode); renderVersion?: number; tooltipsEnabled?: boolean; } export const ModelPickerList: React.FC = ({ providers, favoriteModels, recentModels, modelsMetadata, searchQuery, onSearchQueryChange, onSelect, labels, selectedModel, hiddenModels = [], allowedProviderIds, isModelAllowed, includeNotSelected = false, onSelectNone, selectionCount, disabled = false, maxHeightClassName = 'max-h-[min(400px,calc(100dvh-12rem))] flex-1', maxHeightStyle, sectionHeaderClassName, rowClassName, stickyHeaders = true, autoFocus = true, onEscape, isFavorite, onToggleFavorite, renderRowEnd, onActiveKeyDown, onActiveEntryChange, onVariantKey, onReorderFavorite, reorderFavoriteAriaLabel, reorderFavoriteTitle, providerOrder, onReorderProvider, reorderProviderTitle, footerContent, renderVersion, tooltipsEnabled = true, }) => { const selectionStoreRef = React.useRef(null); if (!selectionStoreRef.current) selectionStoreRef.current = createIndexSelectionStore(); const selectionStore = selectionStoreRef.current; const itemRefs = React.useRef<(HTMLDivElement | null)[]>([]); const scrollRef = React.useRef(null); const stickyFadeSizeRef = React.useRef(0); const sectionHeaderSentinelRefs = React.useRef>(new Map()); const [stuckSectionHeaders, setStuckSectionHeaders] = React.useState>(new Set()); const keyboardOwnsSelectionRef = React.useRef(false); const lastMousePositionRef = React.useRef<{ x: number; y: number } | null>(null); const collapsedRecord = useModelPickerSectionsStore((state) => state.collapsedSections); const toggleSection = useModelPickerSectionsStore((state) => state.toggleSection); const collapsedSections = React.useMemo( () => new Set(Object.keys(collapsedRecord).filter((key) => collapsedRecord[key])), [collapsedRecord], ); const favoriteRowSensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8 } }), ); // Desktop-only provider reordering: a MouseSensor (no TouchSensor) keeps the // section headers tappable/scrollable on touch devices while enabling // click-and-drag with a mouse. const providerSectionSensors = useSensors( useSensor(MouseSensor, { activationConstraint: { distance: 8 } }), ); const allowedProviderSet = React.useMemo(() => { // undefined = no restriction; [] = allow none. Treating empty like // "unrestricted" would resurface providers without a login in pickers that // intentionally pass the authenticated-only list. if (!allowedProviderIds) return null; return new Set(allowedProviderIds); }, [allowedProviderIds]); const providerById = React.useMemo(() => new Map(providers.map((provider) => [provider.id, provider])), [providers]); const isHidden = React.useCallback((providerID: string, modelID: string) => { return hiddenModels.some((hidden) => hidden.providerID === providerID && hidden.modelID === modelID); }, [hiddenModels]); const matchesQuery = React.useCallback( (modelName: string, providerName: string, modelID?: string) => matchesRankQuery([modelName, modelID, providerName], searchQuery), [searchQuery], ); const filteredFavorites = React.useMemo(() => favoriteModels.filter(({ model, providerID, modelID }) => { if (allowedProviderSet && !allowedProviderSet.has(providerID)) return false; if (isModelAllowed && !isModelAllowed(providerID, modelID)) return false; if (isHidden(providerID, modelID)) return false; const providerName = providerById.get(providerID)?.name || providerID; return matchesQuery(getModelDisplayName(model), providerName, modelID); }), [allowedProviderSet, favoriteModels, isHidden, isModelAllowed, matchesQuery, providerById]); const filteredRecents = React.useMemo(() => recentModels.filter(({ model, providerID, modelID }) => { if (allowedProviderSet && !allowedProviderSet.has(providerID)) return false; if (isModelAllowed && !isModelAllowed(providerID, modelID)) return false; if (isHidden(providerID, modelID)) return false; const providerName = providerById.get(providerID)?.name || providerID; return matchesQuery(getModelDisplayName(model), providerName, modelID); }), [allowedProviderSet, isHidden, isModelAllowed, matchesQuery, providerById, recentModels]); const orderedProviders = React.useMemo(() => { if (!providerOrder || providerOrder.length === 0) return providers; const rank = new Map(providerOrder.map((id, index) => [id, index] as const)); const ranked = providers .filter((provider) => rank.has(provider.id)) .sort((a, b) => (rank.get(a.id) ?? 0) - (rank.get(b.id) ?? 0)); const unranked = providers.filter((provider) => !rank.has(provider.id)); return [...ranked, ...unranked]; }, [providerOrder, providers]); const filteredProviders = React.useMemo(() => orderedProviders .filter((provider) => !allowedProviderSet || allowedProviderSet.has(provider.id)) .map((provider) => { const models = Array.isArray(provider.models) ? provider.models : []; const filteredModels = models.filter((model) => { const modelID = typeof model.id === 'string' ? model.id : ''; if (!modelID || isHidden(provider.id, modelID)) return false; if (isModelAllowed && !isModelAllowed(provider.id, modelID)) return false; return matchesQuery(getModelDisplayName(model), provider.name || provider.id, modelID); }); return { ...provider, models: filteredModels }; }) .filter((provider) => provider.models.length > 0), [allowedProviderSet, isHidden, isModelAllowed, matchesQuery, orderedProviders]); const visibleSectionKeys = React.useMemo(() => [ ...(filteredFavorites.length > 0 ? ['favorites'] : []), ...(filteredRecents.length > 0 ? ['recent'] : []), ...filteredProviders.map((provider) => `provider:${provider.id}`), ], [filteredFavorites.length, filteredProviders, filteredRecents.length]); React.useEffect(() => { if (!stickyHeaders || !scrollRef.current) { setStuckSectionHeaders((previous) => previous.size === 0 ? previous : new Set()); return; } const root = scrollRef.current; const observer = new IntersectionObserver((entries) => { setStuckSectionHeaders((previous) => { const next = new Set(previous); let changed = false; for (const entry of entries) { const sectionKey = (entry.target as HTMLElement).dataset.modelSectionKey; if (!sectionKey) continue; const rootTop = entry.rootBounds?.top ?? root.getBoundingClientRect().top; const isAboveScroller = !entry.isIntersecting && entry.boundingClientRect.top < rootTop; if (next.has(sectionKey) === isAboveScroller) continue; changed = true; if (isAboveScroller) next.add(sectionKey); else next.delete(sectionKey); } return changed ? next : previous; }); }, { root, threshold: 0 }); sectionHeaderSentinelRefs.current.forEach((element) => { if (element) observer.observe(element); }); return () => observer.disconnect(); }, [stickyHeaders, visibleSectionKeys]); const syncStickyFade = React.useCallback((scroller: HTMLElement) => { const hasTopScroll = scroller.scrollTop > 1; const fadeSize = hasTopScroll ? Math.min(STICKY_FADE_MIN_SIZE + scroller.scrollTop, STICKY_FADE_MAX_SIZE) : 0; stickyFadeSizeRef.current = fadeSize; scroller.style.setProperty('--scroll-shadow-top-size', `${fadeSize}px`); scroller.style.setProperty( '--scroll-shadow-top-clear-size', `${Math.min(Math.max(fadeSize - 8, 0), STICKY_FADE_CLEAR_MAX_SIZE)}px`, ); }, []); const blockStickyFadeInteraction = React.useCallback(( event: React.MouseEvent | React.PointerEvent, ) => { if ((event.target as Element).closest('[data-overlay-scrollbar-thumb], [data-model-picker-sticky-header]')) return; const eventY = event.clientY - event.currentTarget.getBoundingClientRect().top; if (eventY >= stickyFadeSizeRef.current) return; event.preventDefault(); event.stopPropagation(); }, []); React.useLayoutEffect(() => { if (stickyHeaders && scrollRef.current) syncStickyFade(scrollRef.current); }, [stickyHeaders, syncStickyFade, visibleSectionKeys]); const flatModelList = React.useMemo(() => { const items: ModelPickerEntry[] = []; if (!collapsedSections.has('favorites')) filteredFavorites.forEach((entry) => items.push(entry)); if (!collapsedSections.has('recent')) filteredRecents.forEach((entry) => items.push(entry)); filteredProviders.forEach((provider) => { if (collapsedSections.has(`provider:${provider.id}`)) return; provider.models.forEach((model) => items.push({ model, providerID: provider.id, modelID: model.id as string })); }); return items; }, [collapsedSections, filteredFavorites, filteredProviders, filteredRecents]); const hasResults = flatModelList.length > 0; const favoriteSortingEnabled = Boolean(onReorderFavorite) && searchQuery.trim().length === 0 && filteredFavorites.length > 1; const providerSortingEnabled = Boolean(onReorderProvider) && searchQuery.trim().length === 0 && !allowedProviderSet && filteredProviders.length > 1; const favoriteLookup: Map = React.useMemo(() => new Map( filteredFavorites.map((entry) => [`${entry.providerID}:${entry.modelID}`, entry] as const), ), [filteredFavorites]); React.useEffect(() => { selectionStore.set(0); }, [searchQuery, selectionStore]); const selectIndex = React.useCallback((index: number) => { selectionStore.set(index); onActiveEntryChange?.(flatModelList[index]); }, [flatModelList, onActiveEntryChange, selectionStore]); const moveSelection = React.useCallback((direction: 1 | -1) => { const total = flatModelList.length; if (total === 0) return; keyboardOwnsSelectionRef.current = true; lastMousePositionRef.current = null; const currentIndex = selectionStore.getSnapshot(); const nextIndex = (currentIndex + direction + total) % total; selectionStore.set(nextIndex); onActiveEntryChange?.(flatModelList[nextIndex]); requestAnimationFrame(() => scrollIntoView(scrollRef.current, itemRefs.current[nextIndex])); }, [flatModelList, onActiveEntryChange, selectionStore]); React.useEffect(() => { onActiveEntryChange?.(flatModelList[selectionStore.getSnapshot()]); }, [flatModelList, onActiveEntryChange, selectionStore]); const handleKeyDown = React.useCallback((event: React.KeyboardEvent) => { if (event.defaultPrevented) return; event.stopPropagation(); if ((event.key === 'ArrowLeft' || event.key === 'ArrowRight')) { const selected = flatModelList[selectionStore.getSnapshot()]; if (selected && onVariantKey?.(event, selected)) return; } onActiveKeyDown?.(event, flatModelList[selectionStore.getSnapshot()]); if (event.defaultPrevented) return; if (event.key === 'ArrowDown') { event.preventDefault(); moveSelection(1); return; } if (event.key === 'ArrowUp') { event.preventDefault(); moveSelection(-1); return; } if (event.key === 'Enter') { event.preventDefault(); const selected = flatModelList[selectionStore.getSnapshot()]; if (selected && !disabled) onSelect(selected); return; } if (event.key === 'Escape') { event.preventDefault(); onEscape?.(); } }, [disabled, flatModelList, moveSelection, onActiveKeyDown, onEscape, onSelect, onVariantKey, selectionStore]); const headerClassName = cn( 'typography-micro font-semibold text-muted-foreground uppercase tracking-wider flex items-center gap-2 px-2 py-1.5', stickyHeaders && 'sticky top-0 z-20', sectionHeaderClassName, ); let currentFlatIndex = 0; const renderRow = (entry: ModelPickerEntry, keyPrefix: string, showProviderLogo: boolean, rowIndex: number, dragHandleProps?: SortableFavoriteHandleProps | null) => { const metadata = mergeModelMetadataWithLiveModel(entry.providerID, entry.model, modelsMetadata.get(`${entry.providerID}/${entry.modelID}`)); const contextTokens = formatModelContextTokens(metadata?.limit?.context); const count = selectionCount?.(entry) ?? 0; const isSelected = selectedModel?.providerID === entry.providerID && selectedModel.modelID === entry.modelID; const favorite = isFavorite?.(entry) ?? false; const handleMouseActivity = (event: React.MouseEvent) => { const nextPosition = { x: event.clientX, y: event.clientY }; const previousPosition = lastMousePositionRef.current; const pointerMoved = !previousPosition || previousPosition.x !== nextPosition.x || previousPosition.y !== nextPosition.y; lastMousePositionRef.current = nextPosition; if (keyboardOwnsSelectionRef.current && !previousPosition) return; if (keyboardOwnsSelectionRef.current && !pointerMoved) return; if (keyboardOwnsSelectionRef.current && pointerMoved) keyboardOwnsSelectionRef.current = false; selectIndex(rowIndex); }; return ( {(isHighlighted) => { const rowElement = (
{ itemRefs.current[rowIndex] = el; }} role="option" aria-selected={isSelected} aria-disabled={disabled || undefined} tabIndex={-1} onClick={() => { if (!disabled) onSelect(entry); }} onKeyDown={(event) => { if (disabled) return; if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); onSelect(entry); } }} onMouseEnter={handleMouseActivity} onMouseMove={handleMouseActivity} className={cn( 'w-full text-left px-2 py-1.5 rounded-md typography-meta flex items-center gap-2 cursor-pointer', !disabled && (isHighlighted ? 'bg-interactive-selection' : 'hover:bg-interactive-hover/50'), disabled && 'cursor-not-allowed opacity-60', rowClassName, )} >
{dragHandleProps ? ( ) : null} {showProviderLogo ? : null} {getModelDisplayName(entry.model)} {contextTokens ? {contextTokens} : null}
{count > 0 ? x{count} : null} {renderRowEnd?.(entry, { isHighlighted, isSelected })} {isSelected ? : null} {onToggleFavorite ? ( ) : null}
); return {rowElement}; }}
); }; const handleFavoriteDragEnd = (event: DragEndEvent) => { if (!onReorderFavorite) return; const { active, over } = event; if (!over || active.id === over.id) return; const activeFavorite = favoriteLookup.get(String(active.id)); const overFavorite = favoriteLookup.get(String(over.id)); if (!activeFavorite || !overFavorite) return; onReorderFavorite(activeFavorite, overFavorite); }; const handleProviderDragEnd = (event: DragEndEvent) => { if (!onReorderProvider) return; const { active, over } = event; if (!over || active.id === over.id) return; const ids = orderedProviders.map((provider) => provider.id); const from = ids.indexOf(String(active.id)); const to = ids.indexOf(String(over.id)); if (from === -1 || to === -1) return; onReorderProvider(arrayMove(ids, from, to)); }; const isSectionCollapsed = (key: string) => collapsedSections.has(key); const toggleSectionCollapsed = (key: string) => toggleSection(key); const renderSectionSentinel = (key: string) => stickyHeaders ? (
{ sectionHeaderSentinelRefs.current.set(key, element); }} data-model-section-key={key} className="pointer-events-none absolute top-0 h-px w-full" aria-hidden="true" /> ) : null; const renderSectionHeader = (key: string, icon: React.ReactNode, label: React.ReactNode, headerDragProps?: SortableFavoriteHandleProps) => { const collapsed = isSectionCollapsed(key); const toggleKeyDown = (event: React.KeyboardEvent) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); toggleSectionCollapsed(key); } }; // When the section is reorderable, the whole header acts as the drag // activator (desktop mouse, with an 8px threshold so a plain click still // toggles collapse). A ); }; const renderProviderSection = ( provider: (typeof filteredProviders)[number], providerIndex: number, headerDragProps?: SortableFavoriteHandleProps, ) => { const sectionKey = `provider:${provider.id}`; return ( <> {providerIndex > 0 ?
: null}
{renderSectionSentinel(sectionKey)} {renderSectionHeader(sectionKey, , provider.name || provider.id, headerDragProps)} {!isSectionCollapsed(sectionKey) ? provider.models.map((model) => renderRow({ model, providerID: provider.id, modelID: model.id as string }, 'provider', false, currentFlatIndex++)) : null}
); }; let stuckSectionKey: string | null = null; for (const sectionKey of visibleSectionKeys) { if (stuckSectionHeaders.has(sectionKey)) stuckSectionKey = sectionKey; } // The sidebar can seed its overlay with the first section because its first // header starts flush with the scroller. `Not selected` may precede the // first model section here, so wait for that section's sentinel rather than // showing its identity while the leading action is still visible. const leadingSectionKey = stuckSectionKey ?? (!includeNotSelected ? visibleSectionKeys[0] ?? null : null); const renderSectionIdentity = (sectionKey: string): React.ReactNode => { if (sectionKey === 'favorites') { return <>{labels.favorites}; } if (sectionKey === 'recent') { return <>{labels.recent}; } const providerId = sectionKey.startsWith('provider:') ? sectionKey.slice('provider:'.length) : ''; const provider = providerById.get(providerId); if (!provider) return null; return <>{provider.name || provider.id}; }; return ( <>
onSearchQueryChange(event.target.value)} onKeyDown={handleKeyDown} className="h-7 rounded-none bg-transparent pl-8 pr-0 typography-meta ring-0 hover:[&:not(:focus)]:bg-transparent focus:ring-0 focus-visible:ring-0" autoFocus={autoFocus} />
syncStickyFade(event.currentTarget) : undefined} >
{includeNotSelected ? ( <>
) : null} {!hasResults ? (
{labels.noResults}
) : null} {filteredFavorites.length > 0 ? (
{renderSectionSentinel('favorites')} {renderSectionHeader('favorites', , labels.favorites)} {!isSectionCollapsed('favorites') && (favoriteSortingEnabled ? ( `${entry.providerID}:${entry.modelID}`)} strategy={verticalListSortingStrategy}> {filteredFavorites.map((entry) => { const rowIndex = currentFlatIndex++; return ( {(dragHandleProps) => renderRow(entry, 'fav', true, rowIndex, dragHandleProps)} ); })} ) : filteredFavorites.map((entry) => renderRow(entry, 'fav', true, currentFlatIndex++)))}
) : null} {filteredRecents.length > 0 ? ( <> {filteredFavorites.length > 0 ?
: null}
{renderSectionSentinel('recent')} {renderSectionHeader('recent', , labels.recent)} {!isSectionCollapsed('recent') ? filteredRecents.map((entry) => renderRow(entry, 'recent', true, currentFlatIndex++)) : null}
) : null} {(filteredFavorites.length > 0 || filteredRecents.length > 0) && filteredProviders.length > 0 ?
: null} {providerSortingEnabled ? ( provider.id)} strategy={verticalListSortingStrategy}> {filteredProviders.map((provider, providerIndex) => ( {(dragHandleProps) => renderProviderSection(provider, providerIndex, dragHandleProps)} ))} ) : ( filteredProviders.map((provider, providerIndex) => (
{renderProviderSection(provider, providerIndex)}
)) )}
{stickyHeaders && leadingSectionKey ? ( ) : null}
); };