239 lines · 10.7 KB · TypeScript
RawDownload
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<ProviderInstanceEntry>;  /** 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<ProviderInstanceId>;  /** Non-ready instances whose selected unavailable model remains reachable. */  selectableUnavailableInstanceIds?: ReadonlySet<ProviderInstanceId>;  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<ProviderInstanceId>;}) {  const handleSelect = (instanceId: ProviderInstanceId | "favorites") => {    props.onSelectInstance(instanceId);  };  const showFavorites = props.showFavorites ?? true;  const [hoveredInstanceId, setHoveredInstanceId] = useState<ProviderInstanceId | null>(null);  const sidebarContentRef = useRef<HTMLDivElement>(null);  const [selectedIndicatorTop, setSelectedIndicatorTop] = useState<number | null>(null);  useLayoutEffect(() => {    const content = sidebarContentRef.current;    if (!content) {      return;    }    const selectedItem = Array.from(      content.querySelectorAll<HTMLElement>("[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 (    <div className="w-11 shrink-0 overflow-hidden bg-muted/30" data-model-picker-sidebar="true">      <div className="h-full overflow-y-auto overscroll-contain [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">        <div ref={sidebarContentRef} className="relative flex min-h-full flex-col gap-1 p-1">          {selectedIndicatorTop !== null ? (            <div              data-model-picker-selected-indicator="true"              className={cn(                SELECTED_INDICATOR_CLASS,                "right-0 translate-y-0 transition-[top] duration-200 ease-out",              )}              style={{ top: selectedIndicatorTop }}            />          ) : null}          {/* Favorites section */}          {showFavorites ? (            <>              <div className="relative w-full" data-model-picker-provider="favorites">                <Tooltip>                  <TooltipTrigger                    render={                      <button                        className={cn(                          "relative isolate flex w-full cursor-pointer aspect-square items-center justify-center rounded-md transition-colors hover:bg-[color-mix(in_srgb,var(--popover)_90%,var(--contrast-foreground))] focus-visible:bg-[color-mix(in_srgb,var(--popover)_90%,var(--contrast-foreground))] focus-visible:outline-none",                        )}                        onClick={() => handleSelect("favorites")}                        type="button"                        aria-label="Favorites"                      >                        <StarIcon className="size-5 fill-current shrink-0" aria-hidden />                      </button>                    }                  />                  <TooltipPopup                    side={PICKER_TOOLTIP_SIDE}                    sideOffset={PICKER_TOOLTIP_SIDE_OFFSET}                    align="center"                    className={PICKER_TOOLTIP_CLASS}                  >                    Favorites                  </TooltipPopup>                </Tooltip>              </div>              <div className="border-b border-border/70" aria-hidden="true" />            </>          ) : 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 unavailableSelectionIsReachable =              props.selectableUnavailableInstanceIds?.has(entry.instanceId) ?? false;            const isDisabled =              (isUnavailable && !unavailableSelectionIsReachable) || isContextDisabled;            const isSelected = props.selectedInstanceId === entry.instanceId;            const isHovered = hoveredInstanceId === entry.instanceId;            const showNewBadge = props.newBadgeInstanceIds?.has(entry.instanceId) ?? false;            const showInstanceBadge = shouldShowInstanceBadge(entry, props.instanceEntries);             const tooltip = isUnavailable              ? describeUnavailableInstance(entry)              : isContextDisabled                ? (props.getDisabledInstanceTooltip?.(entry) ?? entry.displayName)                : showNewBadge                  ? `${entry.displayName} — New`                  : entry.displayName;             const button = (              <button                className={cn(                  "relative isolate flex w-full cursor-pointer aspect-square items-center justify-center rounded-md transition-colors hover:bg-[color-mix(in_srgb,var(--popover)_90%,var(--contrast-foreground))] focus-visible:bg-[color-mix(in_srgb,var(--popover)_90%,var(--contrast-foreground))] focus-visible:outline-none",                  isDisabled && "opacity-50 cursor-not-allowed hover:bg-transparent",                )}                data-provider-accent-color={entry.accentColor}                onClick={() => !isDisabled && handleSelect(entry.instanceId)}                onMouseEnter={() => setHoveredInstanceId(entry.instanceId)}                onMouseLeave={() =>                  setHoveredInstanceId((current) => (current === entry.instanceId ? null : current))                }                onFocus={() => setHoveredInstanceId(entry.instanceId)}                onBlur={() =>                  setHoveredInstanceId((current) => (current === entry.instanceId ? null : current))                }                disabled={isDisabled}                type="button"                aria-label={                  isUnavailable || isContextDisabled                    ? tooltip                    : showNewBadge                      ? `${entry.displayName}, new`                      : entry.displayName                }              >                <ProviderInstanceIcon                  driverKind={entry.driverKind}                  displayName={entry.displayName}                  accentColor={entry.accentColor}                  showBadge={showInstanceBadge}                  className="size-6"                  iconClassName="size-5"                  indicatorBackground={                    isHovered && !isDisabled                      ? "var(--muted)"                      : isSelected                        ? "var(--background)"                        : "color-mix(in oklab, var(--muted) 30%, transparent)"                  }                  {...(entry.accentColor                    ? { badgeClassName: "h-3 min-w-3 px-0.5 text-[7px]" }                    : {})}                />                {showNewBadge ? (                  <span className={NEW_BADGE_CLASS} aria-hidden>                    <SparklesIcon className="size-2" />                  </span>                ) : null}              </button>            );             const trigger = isDisabled ? (              <span className="relative block w-full">{button}</span>            ) : (              button            );             return (              <div                key={entry.instanceId}                className="relative w-full"                data-model-picker-provider={entry.instanceId}              >                <Tooltip>                  <TooltipTrigger render={trigger} />                  <TooltipPopup                    side={PICKER_TOOLTIP_SIDE}                    sideOffset={PICKER_TOOLTIP_SIDE_OFFSET}                    align="center"                    className={PICKER_TOOLTIP_CLASS}                  >                    {tooltip}                  </TooltipPopup>                </Tooltip>              </div>            );          })}        </div>      </div>    </div>  );});