import { type ProviderInstanceId } from "@t3tools/contracts"; import { memo, useLayoutEffect, 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, shouldShowInstanceBadge, 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-update-foreground `; /** 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; /** Non-ready instances whose selected unavailable model remains reachable. */ selectableUnavailableInstanceIds?: 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); useLayoutEffect(() => { const content = sidebarContentRef.current; if (!content) { return; } const selectedItem = Array.from( content.querySelectorAll("[data-model-picker-provider]"), ).find((item) => item.dataset.modelPickerProvider === props.selectedInstanceId); if (!selectedItem) { setSelectedIndicatorTop(null); return; } setSelectedIndicatorTop(selectedItem.offsetTop + selectedItem.offsetHeight / 2 - 10); }, [props.instanceEntries, props.selectedInstanceId, showFavorites]); return (
{selectedIndicatorTop !== null ? (
) : null} {/* Favorites section */} {showFavorites ? ( <>
handleSelect("favorites")} type="button" aria-label="Favorites" > } /> Favorites
); });