SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
8.5 KB · 176 lines tsx
Raw Blame History
1"use client";2import * as React from "react";3import { ExternalLink, KeyRound, MoreHorizontal, Trash2, CheckCircle2, XCircle, CircleDashed, CircleOff, Activity, RefreshCw, Zap } from "lucide-react";4import { Button } from "@/components/ui/button";5import { Badge } from "@/components/ui/badge";6import { Tooltip } from "@/components/ui/tooltip";7import { ActionSheet } from "@/components/ui/sheet";8import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator } from "@/components/ui/dropdown-menu";9import { ProviderIcon } from "@/components/brand/provider-icon";10import { PROVIDERS } from "@/lib/client/providers";11import { useIsMobile } from "@/lib/client/hooks";12import { formatRelative, formatMs, formatNumber, cn } from "@/lib/utils";13import type { ProviderId, PublicConnection } from "@/lib/client/types";1415export type ConnectionStatus = "connected" | "failed" | "unverified" | "missing";1617export function statusOf(c: PublicConnection | undefined): ConnectionStatus {18  if (!c) return "missing";19  if (c.status === "valid") return "connected";20  if (c.status === "invalid" || c.status === "error") return "failed";21  return "unverified";22}2324export const STATUS_UI: Record<ConnectionStatus, { label: string; variant: "success" | "danger" | "warning" | "default"; icon: React.ReactNode }> = {25  connected: { label: "Connected", variant: "success", icon: <CheckCircle2 /> },26  failed: { label: "Validation failed", variant: "danger", icon: <XCircle /> },27  unverified: { label: "Not validated yet", variant: "warning", icon: <CircleDashed /> },28  missing: { label: "Not connected", variant: "default", icon: <CircleOff /> },29};3031export interface TestResult {32  ok: boolean;33  latencyMs: number;34  modelsAvailable: number | null;35  error?: string;36  at: number;37}3839export function ProviderRow({ provider, connection, modelCount, testing, lastTest, onAddKey, onTest, onRemove }: { provider: ProviderId; connection?: PublicConnection; modelCount: number | null; testing: boolean; lastTest?: TestResult; onAddKey: () => void; onTest: () => void; onRemove: () => void }) {40  const meta = PROVIDERS[provider];41  const status = statusOf(connection);42  const ui = STATUS_UI[status];43  const isMobile = useIsMobile();44  const [more, setMore] = React.useState(false);45  const models = connection?.modelsAvailable ?? modelCount;46  const lastError = connection?.lastValidationError || connection?.lastErrorCode || null;4748  const moreItems = [49    { key: "replace", label: "Replace key", icon: <KeyRound />, onSelect: onAddKey },50    { key: "test", label: "Test connection", icon: <Activity />, onSelect: onTest, disabled: testing },51    { key: "console", label: `Open ${meta.shortName} console`, icon: <ExternalLink />, onSelect: () => window.open(meta.consoleUrl, "_blank", "noopener,noreferrer") },52    "separator" as const,53    { key: "remove", label: "Remove key", icon: <Trash2 />, onSelect: onRemove, destructive: true },54  ];5556  return (57    <li className={cn("panel p-4 sm:p-5", status === "failed" && "ring-1 ring-danger/30")}>58      <div className="flex items-start gap-3 sm:gap-4">59        <span className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-bg-elevated shadow-xs">60          <ProviderIcon provider={provider} size={20} />61        </span>62        <div className="min-w-0 flex-1">63          <div className="flex flex-wrap items-center gap-x-2 gap-y-1">64            <h3 className="text-[15px] font-semibold leading-6">{meta.name}</h3>65            <Badge variant={ui.variant}>66              {ui.icon} {ui.label}67            </Badge>68          </div>69          <p className="mt-0.5 text-[13px] leading-5 text-fg-muted">{meta.description}</p>7071          {connection ? (72            <dl className="mt-3 grid grid-cols-2 gap-x-4 gap-y-2.5 text-xs sm:grid-cols-4">73              <div>74                <dt className="text-fg-subtle">Key</dt>75                <dd className="mt-0.5 truncate font-mono text-fg">{connection.keyHint}</dd>76              </div>77              <div>78                <dt className="text-fg-subtle">Last validated</dt>79                <dd className="mt-0.5 text-fg">{formatRelative(connection.lastValidatedAt)}</dd>80              </div>81              <div>82                <dt className="text-fg-subtle">Models available</dt>83                <dd className="mt-0.5 text-fg tabular-nums">{models === null ? "—" : formatNumber(models)}</dd>84              </div>85              <div>86                <dt className="text-fg-subtle">{lastTest ? "Latency" : "Last success"}</dt>87                <dd className="mt-0.5 text-fg tabular-nums">88                  {lastTest ? (89                    <span className={cn("inline-flex items-center gap-1", lastTest.ok ? "text-success" : "text-danger")}>90                      <Zap className="size-3" /> {formatMs(lastTest.latencyMs)}91                    </span>92                  ) : (93                    formatRelative(connection.lastSuccessAt)94                  )}95                </dd>96              </div>97              {lastError || (lastTest && !lastTest.ok) ? (98                <div className="col-span-2 sm:col-span-4">99                  <dt className="text-fg-subtle">Last error</dt>100                  <dd className="mt-0.5 flex flex-wrap items-center gap-2 text-danger">101                    {connection.lastErrorCode ? <code className="rounded bg-danger-soft px-1.5 py-0.5 text-[11px]">{connection.lastErrorCode}</code> : null}102                    <span className="text-fg-muted">{lastTest && !lastTest.ok ? lastTest.error : connection.lastValidationError}</span>103                    {connection.lastErrorAt ? <span className="text-fg-subtle">· {formatRelative(connection.lastErrorAt)}</span> : null}104                  </dd>105                </div>106              ) : null}107            </dl>108          ) : (109            <p className="mt-3 text-xs leading-5 text-fg-subtle">110              {meta.keyHelp}{" "}111              <a href={meta.keyDocsUrl} target="_blank" rel="noreferrer noopener" className="tap inline-flex items-center gap-1 py-1.5 inline-flex items-center gap-0.5 text-accent hover:underline">112                Open console <ExternalLink className="size-3" />113              </a>114            </p>115          )}116        </div>117118        {/* Desktop actions */}119        <div className="hidden shrink-0 flex-col items-stretch gap-2 sm:flex">120          {connection ? (121            <>122              <Tooltip content="Re-check the stored key against the provider and refresh its model list">123                <Button size="sm" variant="outline" loading={testing} onClick={onTest}>124                  <Activity /> Test connection125                </Button>126              </Tooltip>127              <DropdownMenu>128                <DropdownMenuTrigger asChild>129                  <Button size="sm" variant="ghost" aria-label="More actions">130                    <MoreHorizontal /> More131                  </Button>132                </DropdownMenuTrigger>133                <DropdownMenuContent align="end">134                  <DropdownMenuItem onSelect={onAddKey}>135                    <KeyRound /> Replace key136                  </DropdownMenuItem>137                  <DropdownMenuItem onSelect={() => window.open(meta.consoleUrl, "_blank", "noopener,noreferrer")}>138                    <ExternalLink /> Open {meta.shortName} console139                  </DropdownMenuItem>140                  <DropdownMenuSeparator />141                  <DropdownMenuItem destructive onSelect={onRemove}>142                    <Trash2 /> Remove key143                  </DropdownMenuItem>144                </DropdownMenuContent>145              </DropdownMenu>146            </>147          ) : (148            <Button size="sm" variant="primary" onClick={onAddKey}>149              <KeyRound /> Add key150            </Button>151          )}152        </div>153      </div>154155      {/* Phone actions: 44 px targets in one row */}156      <div className="mt-3 flex gap-2 sm:hidden">157        {connection ? (158          <>159            <Button size="lg" variant="outline" className="h-11 flex-1" loading={testing} onClick={onTest}>160              {testing ? <RefreshCw /> : <Activity />} Test161            </Button>162            <Button size="lg" variant="ghost" className="h-11 flex-1" onClick={() => setMore(true)} aria-haspopup="menu">163              <MoreHorizontal /> More164            </Button>165          </>166        ) : (167          <Button size="lg" variant="primary" className="h-11 flex-1" onClick={onAddKey}>168            <KeyRound /> Add key169          </Button>170        )}171      </div>172      {isMobile ? <ActionSheet open={more} onOpenChange={setMore} title={meta.name} items={moreItems} /> : null}173    </li>174  );175}176