TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
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