import { type ProviderInstanceId } from "@t3tools/contracts"; import { memo, useLayoutEffect, useMemo, useRef, useState } from "react"; import { SparklesIcon, StarIcon } from "lucide-react"; import { ProviderInstanceIcon } from "./ProviderInstanceIcon"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { cn } from "~/lib/utils"; import { isProviderInstancePickerReady, type ProviderInstanceEntry } from "../../providerInstances"; /** * Build the hover tooltip for an instance button. Mirrors the old * kind-based copy but uses the entry's configured `displayName` so custom * instances get their user-authored name (e.g. "Codex Personal — Unavailable."). */ function describeUnavailableInstance(entry: ProviderInstanceEntry): string { const label = entry.displayName; if (!entry.enabled || entry.status === "disabled") { return `${label} — Disabled in settings.`; } if (entry.status === "ready" && entry.isAvailable) { return label; } const kind = entry.status === "error" ? "Unavailable" : entry.status === "warning" ? "Limited" : "Not ready"; const msg = entry.snapshot.message?.trim(); return msg ? `${label} — ${kind}. ${msg}` : `${label} — ${kind}.`; } const SELECTED_INDICATOR_CLASS = "pointer-events-none absolute -right-1 top-1/2 z-10 h-5 w-0.75 -translate-y-1/2 rounded-l-full bg-primary"; const BADGE_BASE_CLASS = "pointer-events-none absolute -right-0.5 top-0.5 z-10 flex size-3.5 items-center justify-center rounded-full bg-transparent shadow-sm "; const NEW_BADGE_CLASS = `${BADGE_BASE_CLASS} text-amber-600 dark:text-amber-300 `; /** Opens toward the rail so the list stays readable (not over the model names). */ const PICKER_TOOLTIP_SIDE = "left" as const; const PICKER_TOOLTIP_SIDE_OFFSET = 8; const PICKER_TOOLTIP_CLASS = "max-w-64 text-balance font-normal leading-snug"; export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: { selectedInstanceId: ProviderInstanceId | "favorites"; onSelectInstance: (instanceId: ProviderInstanceId | "favorites") => void; /** * Instance entries to render as rail buttons. Each entry becomes one icon * keyed by `instanceId`, so the default built-in Codex and a user-authored * `codex_personal` appear as two distinct rail items, each routing to * their own model list. */ instanceEntries: ReadonlyArray; /** Render the favorites rail entry. Hidden for locked-provider instance switching. */ showFavorites?: boolean; /** Instance ids shown in the rail but unavailable for the current picker context. */ disabledInstanceIds?: ReadonlySet; getDisabledInstanceTooltip?: (entry: ProviderInstanceEntry) => string; /** * Instance id values that should render the "new" sparkle badge. Callers * pass the subset of default built-in ids they want flagged (custom * instances are never flagged — the user just made them). */ newBadgeInstanceIds?: ReadonlySet; }) { const handleSelect = (instanceId: ProviderInstanceId | "favorites") => { props.onSelectInstance(instanceId); }; const showFavorites = props.showFavorites ?? true; const [hoveredInstanceId, setHoveredInstanceId] = useState(null); const sidebarContentRef = useRef(null); const [selectedIndicatorTop, setSelectedIndicatorTop] = useState(null); const duplicateDriverCounts = useMemo(() => { const counts = new Map(); for (const entry of props.instanceEntries) { counts.set(entry.driverKind, (counts.get(entry.driverKind) ?? 0) + 1); } return counts; }, [props.instanceEntries]); useLayoutEffect(() => { const content = sidebarContentRef.current; if (!content) { return; } const selectedButton = Array.from( content.querySelectorAll("[data-model-picker-provider]"), ).find((button) => button.dataset.modelPickerProvider === props.selectedInstanceId); if (!selectedButton) { setSelectedIndicatorTop(null); return; } const contentRect = content.getBoundingClientRect(); const selectedButtonRect = selectedButton.getBoundingClientRect(); setSelectedIndicatorTop( selectedButtonRect.top - contentRect.top + content.scrollTop + selectedButtonRect.height / 2 - 10, ); }, [props.instanceEntries, props.selectedInstanceId, showFavorites]); return (
{selectedIndicatorTop !== null ? (
) : null} {/* Favorites section */} {showFavorites ? (
handleSelect("favorites")} type="button" data-model-picker-provider="favorites" aria-label="Favorites" > } /> Favorites
) : null} {/* Instance buttons (one per configured instance — built-in + custom) */} {props.instanceEntries.map((entry) => { const isUnavailable = !isProviderInstancePickerReady(entry); const isContextDisabled = props.disabledInstanceIds?.has(entry.instanceId) ?? false; const isDisabled = isUnavailable || isContextDisabled; const isSelected = props.selectedInstanceId === entry.instanceId; const isHovered = hoveredInstanceId === entry.instanceId; const showNewBadge = props.newBadgeInstanceIds?.has(entry.instanceId) ?? false; const showInstanceBadge = Boolean(entry.accentColor) || (duplicateDriverCounts.get(entry.driverKind) ?? 0) > 1; const tooltip = isUnavailable ? describeUnavailableInstance(entry) : isContextDisabled ? (props.getDisabledInstanceTooltip?.(entry) ?? entry.displayName) : showNewBadge ? `${entry.displayName} — New` : entry.displayName; const button = ( ); const trigger = isDisabled ? ( {button} ) : ( button ); return (
{tooltip}
); })}
); });