"use client"; import * as React from "react"; import { Coins, Gauge, Timer, Zap } from "lucide-react"; import type { LiveMetrics } from "@/lib/arena/metrics"; import type { ColumnStatus } from "./types"; import { cn, formatMs, formatTokens, formatUsd } from "@/lib/utils"; export const STATUS_META: Record = { idle: { label: "Ready", dot: "bg-fg-subtle", pulse: false }, waiting: { label: "Queued", dot: "bg-fg-subtle", pulse: true }, thinking: { label: "Thinking", dot: "bg-accent", pulse: true }, streaming: { label: "Streaming", dot: "bg-info", pulse: true }, done: { label: "Done", dot: "bg-success", pulse: false }, error: { label: "Error", dot: "bg-danger", pulse: false }, stopped: { label: "Stopped", dot: "bg-warning", pulse: false }, }; export function StatusDot({ status, className }: { status: ColumnStatus; className?: string }) { const s = STATUS_META[status]; return ; } /** A shared clock: re-renders the caller every `ms` while `active`. Returns `Date.now()` snapshots. */ export function useTick(active: boolean, ms = 500): number { const [now, setNow] = React.useState(() => Date.now()); React.useEffect(() => { if (!active) return; // eslint-disable-next-line react-hooks/set-state-in-effect -- clock start setNow(Date.now()); const t = setInterval(() => setNow(Date.now()), ms); return () => clearInterval(t); }, [active, ms]); return now; } export function formatTps(v: number | null): string { return v ? `${v} tok/s` : "—"; } /** * Compact metrics strip shown under each Arena response: status, elapsed/TTFT, tok/s, tokens, cost. * Estimates (while streaming) are prefixed with ≈; exact server numbers are not. */ export function MetricsStrip({ metrics: m, showCosts = true, fastest, cheapest, className }: { metrics: LiveMetrics; showCosts?: boolean; fastest?: boolean; cheapest?: boolean; className?: string }) { const approx = m.exact ? "" : "≈ "; const live = m.status === "waiting" || m.status === "thinking" || m.status === "streaming"; const tokens = m.inputTokens === null && m.outputTokens === null ? "—" : `${formatTokens(m.inputTokens ?? 0)} → ${formatTokens(m.outputTokens ?? 0)}`; return (
} 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} /> } label="Speed" value={formatTps(m.tokensPerSecond)} /> } label="Tokens" value={`${m.exact || tokens === "—" ? "" : approx}${tokens}`} hint={m.reasoningTokens ? `+${formatTokens(m.reasoningTokens)} reasoning` : m.cachedTokens ? `${formatTokens(m.cachedTokens)} cached` : undefined} /> {showCosts ? } 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} } className="hidden sm:block" />
); } function 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 }) { return (
{icon} {label}
{dot} {value}
{hint ?
{hint}
: null}
); }