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