import { type ProviderInstanceId, type ProviderDriverKind, type ResolvedKeybindingsConfig, } from "@t3tools/contracts"; import { memo, useEffect, useMemo, useState } from "react"; import type { VariantProps } from "class-variance-authority"; import { ChevronDownIcon } from "lucide-react"; import { Button, buttonVariants } from "../ui/button"; import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { cn } from "~/lib/utils"; import { ModelPickerContent } from "./ModelPickerContent"; import { ProviderInstanceIcon } from "./ProviderInstanceIcon"; import { ModelEsque, getTriggerDisplayModelLabel, getTriggerDisplayModelName, } from "./providerIconUtils"; import type { ProviderInstanceEntry } from "../../providerInstances"; export const ProviderModelPicker = memo(function ProviderModelPicker(props: { /** * The instance currently selected in the composer. Drives the trigger * icon, label and the default-highlighted combobox row. */ activeInstanceId: ProviderInstanceId; model: string; lockedProvider: ProviderDriverKind | null; lockedContinuationGroupKey?: string | null; /** Instance entries rendered in the sidebar + used to resolve display name. */ instanceEntries: ReadonlyArray; keybindings?: ResolvedKeybindingsConfig; modelOptionsByInstance: ReadonlyMap>; activeProviderIconClassName?: string; compact?: boolean; disabled?: boolean; terminalOpen?: boolean; open?: boolean; triggerVariant?: VariantProps["variant"]; triggerClassName?: string; onOpenChange?: (open: boolean) => void; getModelDisabledReason?: (instanceId: ProviderInstanceId, model: string) => string | null; onInstanceModelChange: (instanceId: ProviderInstanceId, model: string) => void; }) { const [uncontrolledIsMenuOpen, setUncontrolledIsMenuOpen] = useState(false); const isMenuOpen = props.open ?? uncontrolledIsMenuOpen; // Resolve the active instance entry by exact routing key. The composer // resolves fallbacks before rendering this component; if the selected // instance disappears, do not infer a replacement from its driver kind. const activeEntry = useMemo(() => { return ( props.instanceEntries.find((entry) => entry.instanceId === props.activeInstanceId) ?? null ); }, [props.activeInstanceId, props.instanceEntries]); const activeInstanceId = props.activeInstanceId; const selectedInstanceOptions = props.modelOptionsByInstance.get(activeInstanceId) ?? []; // If the current slug belongs to a different instance (for example after // a provider switch or disable), prefer the active instance's first // option so the trigger icon and label stay in sync instead of showing // a stale foreign slug. const selectedModel = selectedInstanceOptions.find((option) => option.slug === props.model) ?? selectedInstanceOptions[0]; const triggerTitle = selectedModel ? getTriggerDisplayModelName(selectedModel) : props.model; const triggerLabel = selectedModel ? getTriggerDisplayModelLabel(selectedModel) : props.model; const duplicateDriverCount = props.instanceEntries.filter( (entry) => activeEntry !== null && entry.driverKind === activeEntry.driverKind, ).length; const showInstanceBadge = Boolean(activeEntry?.accentColor) || duplicateDriverCount > 1; const setIsMenuOpen = (open: boolean) => { props.onOpenChange?.(open); if (props.open === undefined) { setUncontrolledIsMenuOpen(open); } }; useEffect(() => { if (!isMenuOpen) { return; } const { documentElement, body } = document; const previousDocumentOverscrollBehavior = documentElement.style.overscrollBehavior; const previousBodyOverflow = body.style.overflow; const previousBodyPaddingRight = body.style.paddingRight; const scrollbarWidth = window.innerWidth - documentElement.clientWidth; documentElement.style.overscrollBehavior = "contain"; body.style.overflow = "hidden"; if (scrollbarWidth > 0) { body.style.paddingRight = `${scrollbarWidth}px`; } const shouldAllowOverlayScroll = (target: EventTarget | null) => { return target instanceof Element && target.closest("[data-model-picker-content]"); }; const preventBackgroundWheel = (event: WheelEvent) => { if (shouldAllowOverlayScroll(event.target)) { return; } event.preventDefault(); }; const preventBackgroundTouchMove = (event: TouchEvent) => { if (shouldAllowOverlayScroll(event.target)) { return; } event.preventDefault(); }; document.addEventListener("wheel", preventBackgroundWheel, { capture: true, passive: false }); document.addEventListener("touchmove", preventBackgroundTouchMove, { capture: true, passive: false, }); return () => { document.removeEventListener("wheel", preventBackgroundWheel, { capture: true }); document.removeEventListener("touchmove", preventBackgroundTouchMove, { capture: true }); documentElement.style.overscrollBehavior = previousDocumentOverscrollBehavior; body.style.overflow = previousBodyOverflow; body.style.paddingRight = previousBodyPaddingRight; }; }, [isMenuOpen]); const handleInstanceModelChange = (instanceId: ProviderInstanceId, model: string) => { if (props.disabled) return; props.onInstanceModelChange(instanceId, model); setIsMenuOpen(false); }; return ( { if (props.disabled) { setIsMenuOpen(false); return; } setIsMenuOpen(open); }} > } > {activeEntry ? ( ) : null} }> {triggerTitle} {triggerLabel} setIsMenuOpen(false)} {...(props.getModelDisabledReason ? { getModelDisabledReason: props.getModelDisabledReason } : {})} onInstanceModelChange={handleInstanceModelChange} /> ); });