1024 lines · 36.8 KB · TypeScript
RawDownload
"use client"; import {  ArrowUpCircleIcon,  CopyIcon,  DownloadIcon,  LoaderIcon,  LockIcon,  LockOpenIcon,  PlusIcon,  Trash2Icon,  XIcon,} from "lucide-react";import * as Arr from "effect/Array";import * as Result from "effect/Result";import { useEffect, useRef, useState, type ReactNode } from "react";import {  isProviderDriverKind,  resolveProviderInstanceEnabled,  type ProviderInstanceConfig,  type ProviderInstanceEnvironmentVariable,  type ProviderInstanceId,  type ProviderDriverKind,  type ServerProvider,  type ServerProviderModel,} from "@t3tools/contracts"; import {  type CustomModelDefinition,  readCustomModelEntries,  toCustomModelSetting,} from "@t3tools/shared/model";import { cn } from "../../lib/utils";import { useCopyToClipboard } from "../../hooks/useCopyToClipboard";import { normalizeProviderAccentColor } from "../../providerInstances";import { Badge } from "../ui/badge";import { Button } from "../ui/button";import { DraftInput } from "../ui/draft-input";import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";import { Switch } from "../ui/switch";import { stackedThreadToast, toastManager } from "../ui/toast";import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";import type { DriverOption, ProviderEnvironmentFieldDefinition } from "./providerDriverMeta";import { ProviderSettingsForm } from "./ProviderSettingsForm";import { ProviderModelsSection } from "./ProviderModelsSection";import { ProviderInstanceIcon, providerInstanceInitials } from "../chat/ProviderInstanceIcon";import { ProviderAccentColorPicker } from "./ProviderAccentColorPicker";import { RedactedSensitiveText } from "./RedactedSensitiveText";import { SettingsRow, SettingsSection } from "./settingsLayout";import {  getProviderVersionAdvisoryPresentation,  PROVIDER_STATUS_STYLES,  getProviderSummary,  getProviderVersionLabel,  type ProviderStatusKey,} from "./providerStatus"; const ENVIRONMENT_VARIABLE_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/; let environmentVariableDraftId = 0;const nextEnvironmentVariableDraftId = () => `provider-env-${environmentVariableDraftId++}`; type EnvironmentDraftRow = {  readonly id: string;  readonly name: string;  readonly value: string;  readonly sensitive: boolean;  readonly valueRedacted?: boolean;}; function makeEnvironmentDraftRow(  variable: ProviderInstanceEnvironmentVariable,  index: number,): EnvironmentDraftRow {  return {    id: `${index}:${variable.name}`,    name: variable.name,    value: variable.value,    sensitive: variable.sensitive,    ...(variable.valueRedacted !== undefined ? { valueRedacted: variable.valueRedacted } : {}),  };} function providerEnvironmentsEqual(  left: ReadonlyArray<ProviderInstanceEnvironmentVariable>,  right: ReadonlyArray<ProviderInstanceEnvironmentVariable>,): boolean {  return (    left.length === right.length &&    left.every((variable, index) => {      const other = right[index];      return (        other !== undefined &&        variable.name === other.name &&        variable.value === other.value &&        variable.sensitive === other.sensitive &&        variable.valueRedacted === other.valueRedacted      );    })  );} /** * Read `customModels` from the opaque config blob. The concrete driver * schemas type it as `CustomModelSetting[]`, but it arrives here as * `Schema.Unknown`, so the shared reader does the shape checking. */function readConfigCustomModels(config: unknown): ReadonlyArray<CustomModelDefinition> {  if (config === null || typeof config !== "object") return [];  return readCustomModelEntries((config as Record<string, unknown>).customModels);} /** * Set `key` to an arbitrary value on the opaque config blob. Unlike * provider settings field updates, does not drop empty-looking values — the * caller is responsible for deciding whether an empty array / empty * object should be stored explicitly (e.g. `customModels: []` is a * meaningful "user cleared their custom list" state distinct from * "driver default"). */function nextConfigBlobWithValue(  config: unknown,  key: string,  value: unknown,): Record<string, unknown> {  const base: Record<string, unknown> =    config !== null && typeof config === "object" ? { ...(config as Record<string, unknown>) } : {};  base[key] = value;  return base;} /** * Custom rows come from current settings so name/descriptor edits show * instantly; a bare entry falls back to the live row's driver-default * capabilities (the server fills those in on its next probe). */export function deriveProviderModelsForDisplay(input: {  readonly liveModels: ReadonlyArray<ServerProviderModel> | undefined;  readonly customModels: ReadonlyArray<CustomModelDefinition>;}): ReadonlyArray<ServerProviderModel> {  const liveCustomModelsBySlug = new Map(    Arr.filterMap(input.liveModels ?? [], (model) =>      model.isCustom ? Result.succeed([model.slug, model] as const) : Result.failVoid,    ),  );  const serverModels = input.liveModels?.filter((model) => !model.isCustom) ?? [];  const customModels = input.customModels.map((entry) => ({    slug: entry.slug,    name: entry.name,    isCustom: true,    capabilities:      entry.capabilities ?? liveCustomModelsBySlug.get(entry.slug)?.capabilities ?? null,  }));  return [...serverModels, ...customModels];} function ProviderAuthEmail(props: { readonly email: string | undefined }) {  const email = props.email?.trim();  if (!email) return null;   return (    <RedactedSensitiveText      value={email}      ariaLabel="Toggle account email visibility"      revealTooltip="Click to reveal email"      hideTooltip="Click to hide email"      className="max-w-full truncate"    />  );} export function readProviderEnvironmentVariable(  environment: ReadonlyArray<ProviderInstanceEnvironmentVariable> | undefined,  name: string,): ProviderInstanceEnvironmentVariable | undefined {  return environment?.find((variable) => variable.name === name);} export function providerEnvironmentWithoutNames(  environment: ReadonlyArray<ProviderInstanceEnvironmentVariable> | undefined,  names: ReadonlySet<string>,): ReadonlyArray<ProviderInstanceEnvironmentVariable> {  return (environment ?? []).filter((variable) => !names.has(variable.name));} export function nextProviderEnvironmentWithFieldValue(  environment: ReadonlyArray<ProviderInstanceEnvironmentVariable> | undefined,  field: ProviderEnvironmentFieldDefinition,  value: string,): ReadonlyArray<ProviderInstanceEnvironmentVariable> {  const trimmed = value.trim();  const next: ProviderInstanceEnvironmentVariable[] = [];  let found = false;   for (const variable of environment ?? []) {    if (variable.name !== field.name) {      next.push(variable);      continue;    }    found = true;    if (trimmed.length > 0) {      next.push({        name: variable.name,        value: trimmed,        sensitive: field.sensitive ?? true,      });    }  }   if (!found && trimmed.length > 0) {    next.push({      name: field.name,      value: trimmed,      sensitive: field.sensitive ?? true,    });  }   return next;} function ProviderEnvironmentFieldRow(props: {  readonly field: ProviderEnvironmentFieldDefinition;  readonly variable: ProviderInstanceEnvironmentVariable | undefined;  readonly idPrefix: string;  readonly onCommit: (field: ProviderEnvironmentFieldDefinition, value: string) => void;  readonly onRemove: (field: ProviderEnvironmentFieldDefinition) => void;}) {  const inputId = `${props.idPrefix}-environment-${props.field.name}`;  const value = props.variable?.valueRedacted ? "" : (props.variable?.value ?? "");  const placeholder = props.variable?.valueRedacted    ? "Stored secret - enter a new value to replace"    : props.field.placeholder;   return (    <label htmlFor={inputId} className="block">      <span className="text-xs font-medium text-foreground">{props.field.label}</span>      <div className="mt-1.5 flex min-w-0 items-center gap-2">        <DraftInput          id={inputId}          className="min-w-0 flex-1"          type={props.field.sensitive === false ? undefined : "password"}          autoComplete="off"          value={value}          onCommit={(next) => props.onCommit(props.field, next)}          placeholder={placeholder}          spellCheck={false}        />        {props.variable ? (          <Button            type="button"            size="icon-sm"            variant="ghost"            className="size-8 shrink-0 text-muted-foreground hover:text-destructive"            onClick={() => props.onRemove(props.field)}            aria-label={`Clear ${props.field.label}`}          >            <XIcon className="size-3.5" />          </Button>        ) : null}      </div>      {props.field.description ? (        <span className="mt-1 block text-xs text-muted-foreground">{props.field.description}</span>      ) : null}    </label>  );} function ProviderEnvironmentSection(props: {  readonly environment: ReadonlyArray<ProviderInstanceEnvironmentVariable>;  readonly onChange: (environment: ReadonlyArray<ProviderInstanceEnvironmentVariable>) => void;}) {  const [rows, setRows] = useState<ReadonlyArray<EnvironmentDraftRow>>(() =>    props.environment.map(makeEnvironmentDraftRow),  );  const previousEnvironmentRef = useRef(props.environment);  const lastPublishedEnvironmentRef = useRef<    ReadonlyArray<ProviderInstanceEnvironmentVariable> | undefined  >(undefined);   useEffect(() => {    const previousEnvironment = previousEnvironmentRef.current;    const lastPublishedEnvironment = lastPublishedEnvironmentRef.current;    previousEnvironmentRef.current = props.environment;    lastPublishedEnvironmentRef.current = undefined;    if (      previousEnvironment === props.environment ||      providerEnvironmentsEqual(previousEnvironment, props.environment) ||      (lastPublishedEnvironment !== undefined &&        providerEnvironmentsEqual(lastPublishedEnvironment, props.environment))    ) {      return;    }    setRows(props.environment.map(makeEnvironmentDraftRow));  }, [props.environment]);   const publishRows = (nextRows: ReadonlyArray<EnvironmentDraftRow>) => {    const published: ProviderInstanceEnvironmentVariable[] = [];    for (const row of nextRows) {      const name = row.name.trim();      if (!ENVIRONMENT_VARIABLE_NAME_PATTERN.test(name)) {        if (          name.length > 0 ||          row.value.length > 0 ||          row.sensitive !== true ||          row.valueRedacted !== undefined        ) {          return;        }        continue;      }      const { id: _id, ...rest } = row;      published.push({ ...rest, name });    }    lastPublishedEnvironmentRef.current = published;    props.onChange(published);  };   const updateVariable = (id: string, patch: Partial<Omit<EnvironmentDraftRow, "id">>) => {    const nextRows = rows.map((row) =>      row.id === id        ? {            ...row,            ...patch,            ...(patch.value !== undefined ? { valueRedacted: false } : {}),          }        : row,    );    setRows(nextRows);    publishRows(nextRows);  };   const removeVariable = (id: string) => {    const nextRows = rows.filter((row) => row.id !== id);    setRows(nextRows);    publishRows(nextRows);  };   const addVariable = () =>    setRows([      ...rows,      {        id: nextEnvironmentVariableDraftId(),        name: "",        value: "",        sensitive: true,      },    ]);   return (    <div className="mt-3 min-w-0 space-y-2">      {rows.map((variable, index) => (        <div key={variable.id} className="flex min-w-0 items-center gap-1.5">          <DraftInput            size="sm"            className="w-44 shrink-0 font-mono"            value={variable.name}            onCommit={(name) => updateVariable(variable.id, { name: name.trim() })}            placeholder="VARIABLE_NAME"            spellCheck={false}            aria-label={`Environment variable name ${index + 1}`}          />          <span className="text-xs text-muted-foreground" aria-hidden>            =          </span>          <DraftInput            size="sm"            className="min-w-0 flex-1 font-mono"            value={variable.valueRedacted ? "" : variable.value}            onCommit={(value) => updateVariable(variable.id, { value })}            type={variable.sensitive ? "password" : undefined}            autoComplete="off"            placeholder={              variable.valueRedacted ? "Stored secret, enter a new value to replace" : "value"            }            spellCheck={false}            aria-label={`Environment variable value ${index + 1}`}          />          <Tooltip>            <TooltipTrigger              render={                <Button                  type="button"                  size="icon-micro"                  variant="ghost-muted"                  className={cn(                    "[--control-icon-color:currentColor]",                    variable.sensitive && "text-foreground",                  )}                  onClick={() => {                    const sensitive = !variable.sensitive;                    updateVariable(variable.id, {                      sensitive,                      ...(sensitive && variable.valueRedacted === undefined                        ? {}                        : { valueRedacted: sensitive ? variable.valueRedacted : false }),                    });                  }}                  aria-pressed={variable.sensitive}                  aria-label={`Mark environment variable ${variable.name || index + 1} as sensitive`}                >                  {variable.sensitive ? (                    <LockIcon className="size-3" />                  ) : (                    <LockOpenIcon className="size-3" />                  )}                </Button>              }            />            <TooltipPopup side="top">              {variable.sensitive ? "Sensitive, stored separately" : "Plain text"}            </TooltipPopup>          </Tooltip>          <Button            type="button"            size="icon-micro"            variant="ghost-muted"            className="[--control-icon-color:currentColor] hover:text-destructive"            onClick={() => removeVariable(variable.id)}            aria-label={`Remove environment variable ${variable.name || index + 1}`}          >            <XIcon className="size-3" />          </Button>        </div>      ))}      <div className="flex min-h-[1.875rem] flex-wrap items-center justify-end gap-x-3 gap-y-1">        {rows.length > 0 ? (          <span className="mr-auto text-xs text-muted-foreground">            Sensitive values are stored separately and never returned to the app.          </span>        ) : null}        <Button type="button" size="xs" variant="ghost-muted" onClick={addVariable}>          <PlusIcon className="size-3" />          Add variable        </Button>      </div>    </div>  );} interface ProviderInstanceCardProps {  readonly instanceId: ProviderInstanceId;  readonly instance: ProviderInstanceConfig;  readonly driverOption: DriverOption | undefined;  readonly liveProvider: ServerProvider | undefined;  readonly mode: "list" | "editor";  readonly selected?: boolean | undefined;  readonly onSelect?: (() => void) | undefined;  readonly readOnly?: boolean | undefined;  readonly onUpdate: (nextInstance: ProviderInstanceConfig) => void;  /**   * Pass `undefined` to hide the delete footer entirely. Built-in default   * instance slots use `undefined` — they can't be deleted without losing   * the slot, and their "reset to defaults" affordance lives on an outer   * reset button instead. Explicit `| undefined` in the type accommodates   * `exactOptionalPropertyTypes: true`, where an absent key and   * `{ onDelete: undefined }` are treated as distinct shapes.   */  readonly onDelete?: (() => void) | undefined;  /**   * Optional outer reset button rendered next to the driver icon. Built-in   * default slots supply a reset-to-factory control here; custom instances   * omit it.   */  readonly headerAction?: ReactNode | undefined;  readonly setup?: ReactNode;  readonly hiddenModels: ReadonlyArray<string>;  readonly favoriteModels: ReadonlyArray<string>;  readonly modelOrder: ReadonlyArray<string>;  readonly onHiddenModelsChange: (next: ReadonlyArray<string>) => void;  readonly onFavoriteModelsChange: (next: ReadonlyArray<string>) => void;  readonly onModelOrderChange: (next: ReadonlyArray<string>) => void;  readonly onRunUpdate?: (() => void) | undefined;  readonly isUpdating?: boolean | undefined;} /** * Renders one provider instance as either a compact selectable list row or * the full editor shown beside that list. Both modes use the same enabled * state and provider metadata. * * Behavior notes: *   - `liveProvider` is matched by the caller via `instanceId`; when no *     match is available (e.g. the server hasn't probed yet, or the *     driver is not shipped by the current build) the card still renders *     with a neutral "checking" summary. *   - Unknown drivers (`driverOption === undefined`) get a read-only *     notice instead of editable fields, so fork instances round-trip *     without accidentally destroying their config. *   - The enabled Switch writes to the envelope's `instance.enabled` *     field, which is the single enabled flag: the server folds any legacy *     driver-specific `config.enabled` into the envelope on load and both *     sides resolve through `resolveProviderInstanceEnabled` (an explicit *     false wins, then envelope, then config, then the driver default). */export function ProviderInstanceCard({  instanceId,  instance,  driverOption,  liveProvider,  mode,

Lines 1–500 of 1024.