SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
14 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
7.4 KB · 152 lines tsx
Raw Blame History
1"use client";2import * as React from "react";3import { Activity, MoreHorizontal, Pencil, RefreshCw, Trash2, CheckCircle2, XCircle, CircleDashed, Zap, Lock, Globe } 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 { useIsMobile } from "@/lib/client/hooks";11import { formatRelative, formatMs, formatNumber, cn } from "@/lib/utils";12import type { PublicEndpoint } from "@/lib/client/types";1314const STATUS: Record<PublicEndpoint["status"], { label: string; variant: "success" | "danger" | "warning" | "default"; icon: React.ReactNode }> = {15  valid: { label: "Reachable", variant: "success", icon: <CheckCircle2 /> },16  invalid: { label: "Unreachable", variant: "danger", icon: <XCircle /> },17  error: { label: "Error", variant: "danger", icon: <XCircle /> },18  unverified: { label: "Not tested", variant: "warning", icon: <CircleDashed /> },19};2021function hostOf(url: string): string {22  try {23    const u = new URL(url);24    return `${u.host}${u.pathname.replace(/\/$/, "")}`;25  } catch {26    return url;27  }28}2930export function EndpointRow({ endpoint, testing, syncing, onTest, onSync, onEdit, onRemove }: { endpoint: PublicEndpoint; testing: boolean; syncing: boolean; onTest: () => void; onSync: () => void; onEdit: () => void; onRemove: () => void }) {31  const ui = STATUS[endpoint.status] ?? STATUS.unverified;32  const isMobile = useIsMobile();33  const [more, setMore] = React.useState(false);34  const total = endpoint.modelsAvailable ?? endpoint.discoveredModels.length + endpoint.manualModels.length;35  const items = [36    { key: "edit", label: "Edit endpoint", icon: <Pencil />, onSelect: onEdit },37    { key: "sync", label: "Refresh model list", icon: <RefreshCw />, onSelect: onSync, disabled: !endpoint.modelsPath || syncing },38    "separator" as const,39    { key: "remove", label: "Remove endpoint", icon: <Trash2 />, onSelect: onRemove, destructive: true },40  ];41  return (42    <li className={cn("panel p-4 sm:p-5", endpoint.status === "invalid" && "ring-1 ring-danger/30")}>43      <div className="flex items-start gap-3 sm:gap-4">44        <span className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-bg-elevated shadow-xs">45          <ProviderIcon provider="custom" size={20} />46        </span>47        <div className="min-w-0 flex-1">48          <div className="flex flex-wrap items-center gap-x-2 gap-y-1">49            <h3 className="truncate text-[15px] font-semibold leading-6">{endpoint.name}</h3>50            <Badge variant={ui.variant}>51              {ui.icon} {ui.label}52            </Badge>53            {endpoint.isPrivate ? (54              <Tooltip content="Private / local address — only reachable when PolyLLM runs on the same network.">55                <Badge variant="outline">56                  <Lock /> local57                </Badge>58              </Tooltip>59            ) : (60              <Badge variant="outline">61                <Globe /> {endpoint.baseUrl.startsWith("https") ? "https" : "http"}62              </Badge>63            )}64          </div>65          <p className="mt-0.5 truncate font-mono text-[12.5px] text-fg-muted">{hostOf(endpoint.baseUrl)}</p>66          <dl className="mt-3 grid grid-cols-2 gap-x-4 gap-y-2.5 text-xs sm:grid-cols-4">67            <div>68              <dt className="text-fg-subtle">Models</dt>69              <dd className="mt-0.5 text-fg tabular-nums">70                {formatNumber(total)}71                {endpoint.manualModels.length ? <span className="text-fg-subtle"> · {endpoint.manualModels.length} manual</span> : null}72              </dd>73            </div>74            <div>75              <dt className="text-fg-subtle">Latency</dt>76              <dd className={cn("mt-0.5 inline-flex items-center gap-1 tabular-nums", endpoint.status === "valid" ? "text-success" : "text-fg")}>77                {endpoint.lastLatencyMs !== null ? (78                  <>79                    <Zap className="size-3" /> {formatMs(endpoint.lastLatencyMs)}80                  </>81                ) : (82                  "—"83                )}84              </dd>85            </div>86            <div>87              <dt className="text-fg-subtle">Last tested</dt>88              <dd className="mt-0.5 text-fg">{formatRelative(endpoint.lastValidatedAt)}</dd>89            </div>90            <div>91              <dt className="text-fg-subtle">Auth</dt>92              <dd className="mt-0.5 truncate font-mono text-fg">{endpoint.hasKey ? endpoint.keyHint : endpoint.headerNames.length ? `${endpoint.headerNames.length} header${endpoint.headerNames.length === 1 ? "" : "s"}` : "none"}</dd>93            </div>94            {endpoint.lastValidationError ? (95              <div className="col-span-2 sm:col-span-4">96                <dt className="text-fg-subtle">Last error</dt>97                <dd className="mt-0.5 text-danger">{endpoint.lastValidationError}</dd>98              </div>99            ) : null}100          </dl>101          {endpoint.discoveredModels.length ? (102            <ul className="mt-2.5 flex flex-wrap gap-1.5">103              {endpoint.discoveredModels.slice(0, isMobile ? 4 : 8).map((m) => (104                <li key={m.id}>105                  <Badge variant="outline" className="max-w-[200px] truncate font-mono text-[10.5px] text-fg-muted">106                    {m.id}107                  </Badge>108                </li>109              ))}110              {endpoint.discoveredModels.length > (isMobile ? 4 : 8) ? <li className="text-[11px] text-fg-subtle">+{endpoint.discoveredModels.length - (isMobile ? 4 : 8)} more</li> : null}111            </ul>112          ) : null}113        </div>114115        <div className="hidden shrink-0 flex-col items-stretch gap-2 sm:flex">116          <Button size="sm" variant="outline" loading={testing} onClick={onTest}>117            <Activity /> Test connection118          </Button>119          <DropdownMenu>120            <DropdownMenuTrigger asChild>121              <Button size="sm" variant="ghost" aria-label="More actions">122                <MoreHorizontal /> More123              </Button>124            </DropdownMenuTrigger>125            <DropdownMenuContent align="end">126              <DropdownMenuItem onSelect={onEdit}>127                <Pencil /> Edit endpoint128              </DropdownMenuItem>129              <DropdownMenuItem onSelect={onSync} disabled={!endpoint.modelsPath || syncing}>130                <RefreshCw /> Refresh model list131              </DropdownMenuItem>132              <DropdownMenuSeparator />133              <DropdownMenuItem destructive onSelect={onRemove}>134                <Trash2 /> Remove endpoint135              </DropdownMenuItem>136            </DropdownMenuContent>137          </DropdownMenu>138        </div>139      </div>140      <div className="mt-3 flex gap-2 sm:hidden">141        <Button size="lg" variant="outline" className="h-11 flex-1" loading={testing} onClick={onTest}>142          <Activity /> Test143        </Button>144        <Button size="lg" variant="ghost" className="h-11 flex-1" onClick={() => setMore(true)} aria-haspopup="menu">145          <MoreHorizontal /> More146        </Button>147      </div>148      {isMobile ? <ActionSheet open={more} onOpenChange={setMore} title={endpoint.name} description={hostOf(endpoint.baseUrl)} items={items} /> : null}149    </li>150  );151}152