210 lines · 8.0 KB · TypeScript
RawDownload
import { type ProviderKind, type ServerProvider } from "@t3tools/contracts";import { memo } from "react";import { Clock3Icon, SparklesIcon, StarIcon } from "lucide-react";import { Gemini, GithubCopilotIcon } from "../Icons";import { AVAILABLE_PROVIDER_OPTIONS, PROVIDER_ICON_BY_PROVIDER } from "./providerIconUtils";import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";import { cn } from "~/lib/utils";import { getProviderSnapshot } from "../../providerModels"; function describeUnavailableProvider(label: string, live: ServerProvider | undefined): string {  if (!live) {    return `${label} — waiting for provider status…`;  }  if (live.status === "ready") {    return label;  }  const kind =    live.status === "error"      ? "Unavailable"      : live.status === "warning"        ? "Limited"        : live.status === "disabled"          ? "Disabled in settings"          : "Not ready";  const msg = live.message?.trim();  return msg ? `${label} — ${kind}. ${msg}` : `${label} — ${kind}.`;} const SELECTED_BUTTON_CLASS = "bg-background text-foreground shadow-sm";const SELECTED_INDICATOR_CLASS =  "pointer-events-none absolute -right-1 top-1/2 z-10 h-5 w-0.5 -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 `;const SOON_BADGE_CLASS = `${BADGE_BASE_CLASS} text-muted-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_CLASS = "max-w-64 text-balance font-normal leading-snug"; export const ModelPickerSidebar = memo(function ModelPickerSidebar(props: {  selectedProvider: ProviderKind | "favorites";  onSelectProvider: (provider: ProviderKind | "favorites") => void;  providers?: ReadonlyArray<ServerProvider>;}) {  const handleProviderClick = (provider: ProviderKind | "favorites") => {    props.onSelectProvider(provider);  };   return (    <div className="flex flex-col w-12 border-r bg-muted/30  p-1 overflow-y-auto gap-1">      {/* Favorites section */}      <div className="pb-1 mb-1 border-b">        <div className="relative w-full">          {props.selectedProvider === "favorites" && <div className={SELECTED_INDICATOR_CLASS} />}          <Tooltip>            <TooltipTrigger              render={                <button                  className={cn(                    "relative isolate flex w-full cursor-pointer aspect-square items-center justify-center rounded transition-colors hover:bg-muted",                    props.selectedProvider === "favorites" && SELECTED_BUTTON_CLASS,                  )}                  onClick={() => handleProviderClick("favorites")}                  type="button"                  data-model-picker-provider="favorites"                  aria-label="Favorites"                >                  <StarIcon className="size-5 fill-current shrink-0" aria-hidden />                </button>              }            />            <TooltipPopup              side={PICKER_TOOLTIP_SIDE}              align="center"              className={PICKER_TOOLTIP_CLASS}            >              Favorites            </TooltipPopup>          </Tooltip>        </div>      </div>       {/* Provider buttons */}      {AVAILABLE_PROVIDER_OPTIONS.map((option) => {        const OptionIcon = PROVIDER_ICON_BY_PROVIDER[option.value];        const liveProvider = props.providers          ? getProviderSnapshot(props.providers, option.value)          : undefined;         const isDisabled = !liveProvider || liveProvider.status !== "ready";        const isSelected = props.selectedProvider === option.value;        const badge = option.pickerSidebarBadge;         const providerTooltip = isDisabled          ? describeUnavailableProvider(option.label, liveProvider)          : badge === "new"            ? `${option.label} — New`            : option.label;         const button = (          <button            data-model-picker-provider={option.value}            className={cn(              "relative isolate flex w-full cursor-pointer aspect-square items-center justify-center rounded transition-colors hover:bg-muted",              isSelected && SELECTED_BUTTON_CLASS,              isDisabled && "opacity-50 cursor-not-allowed hover:bg-transparent",            )}            onClick={() => !isDisabled && handleProviderClick(option.value)}            disabled={isDisabled}            type="button"            aria-label={              isDisabled                ? (providerTooltip ?? option.label)                : badge === "new"                  ? `${option.label}, new`                  : option.label            }          >            <OptionIcon className="size-5 shrink-0" aria-hidden />            {badge === "new" ? (              <span className={NEW_BADGE_CLASS} aria-hidden>                <SparklesIcon className="size-2" />              </span>            ) : badge === "soon" ? (              <span className={SOON_BADGE_CLASS} aria-hidden>                <Clock3Icon className="size-2" />              </span>            ) : null}          </button>        );         const trigger = isDisabled ? (          <span className="relative block w-full">{button}</span>        ) : (          button        );         return (          <div key={option.value} className="relative w-full">            {isSelected && <div className={SELECTED_INDICATOR_CLASS} />}            <Tooltip>              <TooltipTrigger render={trigger} />              <TooltipPopup                side={PICKER_TOOLTIP_SIDE}                align="center"                className={PICKER_TOOLTIP_CLASS}              >                {providerTooltip}              </TooltipPopup>            </Tooltip>          </div>        );      })}       {/* Gemini button (coming soon) */}      <Tooltip>        <TooltipTrigger          render={            <span className="relative block w-full">              <button                className={cn(                  "relative isolate flex w-full aspect-square items-center justify-center rounded opacity-50 cursor-not-allowed transition-colors hover:bg-transparent",                )}                disabled                type="button"                data-model-picker-provider="gemini-coming-soon"                aria-label="Gemini — coming soon"              >                <Gemini className="size-5 text-muted-foreground/85" aria-hidden />                <span className={SOON_BADGE_CLASS} aria-hidden>                  <Clock3Icon className="size-2" />                </span>              </button>            </span>          }        />        <TooltipPopup side={PICKER_TOOLTIP_SIDE} align="center" className={PICKER_TOOLTIP_CLASS}>          Gemini — Coming soon        </TooltipPopup>      </Tooltip>      {/* Github Copilot button (coming soon) */}      <Tooltip>        <TooltipTrigger          render={            <span className="relative block w-full">              <button                className={cn(                  "relative isolate flex w-full aspect-square items-center justify-center rounded opacity-50 cursor-not-allowed transition-colors hover:bg-transparent",                )}                disabled                type="button"                data-model-picker-provider="github-copilot-coming-soon"                aria-label="Github Copilot — coming soon"              >                <GithubCopilotIcon className="size-5 text-muted-foreground/85" aria-hidden />                <span className={SOON_BADGE_CLASS} aria-hidden>                  <Clock3Icon className="size-2" />                </span>              </button>            </span>          }        />        <TooltipPopup side={PICKER_TOOLTIP_SIDE} align="center" className={PICKER_TOOLTIP_CLASS}>          Github Copilot — Coming soon        </TooltipPopup>      </Tooltip>    </div>  );});