TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1"use client";2import * as React from "react";3import { Coins, Gauge, Timer, Zap } from "lucide-react";4import type { LiveMetrics } from "@/lib/arena/metrics";5import type { ColumnStatus } from "./types";6import { cn, formatMs, formatTokens, formatUsd } from "@/lib/utils";78export const STATUS_META: Record<ColumnStatus, { label: string; dot: string; pulse: boolean }> = {9 idle: { label: "Ready", dot: "bg-fg-subtle", pulse: false },10 waiting: { label: "Queued", dot: "bg-fg-subtle", pulse: true },11 thinking: { label: "Thinking", dot: "bg-accent", pulse: true },12 streaming: { label: "Streaming", dot: "bg-info", pulse: true },13 done: { label: "Done", dot: "bg-success", pulse: false },14 error: { label: "Error", dot: "bg-danger", pulse: false },15 stopped: { label: "Stopped", dot: "bg-warning", pulse: false },16};1718export function StatusDot({ status, className }: { status: ColumnStatus; className?: string }) {19 const s = STATUS_META[status];20 return <span className={cn("inline-block size-2 shrink-0 rounded-full", s.dot, s.pulse && "animate-pulse-soft", className)} aria-hidden />;21}2223/** A shared clock: re-renders the caller every `ms` while `active`. Returns `Date.now()` snapshots. */24export function useTick(active: boolean, ms = 500): number {25 const [now, setNow] = React.useState(() => Date.now());26 React.useEffect(() => {27 if (!active) return;28 // eslint-disable-next-line react-hooks/set-state-in-effect -- clock start29 setNow(Date.now());30 const t = setInterval(() => setNow(Date.now()), ms);31 return () => clearInterval(t);32 }, [active, ms]);33 return now;34}3536export function formatTps(v: number | null): string {37 return v ? `${v} tok/s` : "—";38}3940/**41 * Compact metrics strip shown under each Arena response: status, elapsed/TTFT, tok/s, tokens, cost.42 * Estimates (while streaming) are prefixed with ≈; exact server numbers are not.43 */44export function MetricsStrip({ metrics: m, showCosts = true, fastest, cheapest, className }: { metrics: LiveMetrics; showCosts?: boolean; fastest?: boolean; cheapest?: boolean; className?: string }) {45 const approx = m.exact ? "" : "≈ ";46 const live = m.status === "waiting" || m.status === "thinking" || m.status === "streaming";47 const tokens = m.inputTokens === null && m.outputTokens === null ? "—" : `${formatTokens(m.inputTokens ?? 0)} → ${formatTokens(m.outputTokens ?? 0)}`;48 return (49 <dl className={cn("grid grid-cols-3 gap-x-3 gap-y-1.5 text-[11.5px] tabular-nums sm:grid-cols-5", className)} aria-live={live ? "polite" : undefined}>50 <Metric icon={<Timer />} label={m.ttftMs !== null ? "TTFT" : "Elapsed"} value={m.ttftMs !== null ? formatMs(m.ttftMs) : formatMs(m.elapsedMs)} highlight={fastest} hint={m.ttftMs !== null && live ? `${formatMs(m.elapsedMs)} total` : m.ttftMs !== null && !live ? `${formatMs(m.elapsedMs)} total` : undefined} />51 <Metric icon={<Zap />} label="Speed" value={formatTps(m.tokensPerSecond)} />52 <Metric icon={<Gauge />} label="Tokens" value={`${m.exact || tokens === "—" ? "" : approx}${tokens}`} hint={m.reasoningTokens ? `+${formatTokens(m.reasoningTokens)} reasoning` : m.cachedTokens ? `${formatTokens(m.cachedTokens)} cached` : undefined} />53 {showCosts ? <Metric icon={<Coins />} label="Cost" value={m.costUsd === null ? "—" : `${approx}${formatUsd(m.costUsd, { precise: m.costUsd < 0.01 })}`} highlight={cheapest} title={m.exact ? "From provider usage and list price" : "Estimated from characters streamed so far"} /> : null}54 <Metric label="Status" value={STATUS_META[m.status].label} dot={<StatusDot status={m.status} />} className="hidden sm:block" />55 </dl>56 );57}5859function Metric({ icon, label, value, hint, highlight, title, dot, className }: { icon?: React.ReactNode; label: string; value: string; hint?: string; highlight?: boolean; title?: string; dot?: React.ReactNode; className?: string }) {60 return (61 <div className={cn("min-w-0", className)} title={title}>62 <dt className="flex items-center gap-1 text-[10.5px] uppercase tracking-wide text-fg-subtle [&_svg]:size-3">63 {icon}64 {label}65 </dt>66 <dd className={cn("flex items-center gap-1.5 truncate font-medium", highlight ? "text-success" : "text-fg")}>67 {dot}68 {value}69 </dd>70 {hint ? <dd className="truncate text-[10.5px] text-fg-subtle">{hint}</dd> : null}71 </div>72 );73}74