"use client"; import * as React from "react"; import { Trophy } from "lucide-react"; import { ProviderIcon } from "@/components/brand/provider-icon"; import type { LiveMetrics } from "@/lib/arena/metrics"; import { criterionById, type Criterion } from "@/lib/arena/scoring"; import { cn, formatMs, formatTokens, formatUsd } from "@/lib/utils"; import { STATUS_META, StatusDot, formatTps } from "./metrics-strip"; import { BlindAvatar } from "./blind"; export interface ComparisonRow { key: string; name: string; provider: string | null; /** Blind: identity hidden, show letter avatar. */ hidden?: boolean; blindIndex: number; metrics: LiveMetrics; criteriaWon: string[]; isWinner: boolean; } interface Props { rows: ComparisonRow[]; showCosts?: boolean; customCriteria?: Criterion[]; className?: string; } type Col = { id: string; label: string; get: (r: ComparisonRow) => string; best?: (rows: ComparisonRow[]) => string | null }; function bestBy(rows: ComparisonRow[], get: (r: ComparisonRow) => number | null, dir: "min" | "max"): string | null { const cands = rows.map((r) => ({ key: r.key, v: get(r) })).filter((x): x is { key: string; v: number } => typeof x.v === "number" && Number.isFinite(x.v)); if (cands.length < 2) return null; cands.sort((a, b) => (dir === "min" ? a.v - b.v : b.v - a.v)); return cands[0].v === cands[1].v ? null : cands[0].key; } /** * End-of-run comparison. Table from `md` up; stacked metric rows on phones (never a 4-column table on a phone). */ export function ComparisonTable({ rows, showCosts = true, customCriteria = [], className }: Props) { const cols = React.useMemo(() => { const list: Col[] = [ { id: "status", label: "Status", get: (r) => STATUS_META[r.metrics.status].label }, { id: "ttft", label: "TTFT", get: (r) => formatMs(r.metrics.ttftMs), best: (rs) => bestBy(rs, (r) => r.metrics.ttftMs, "min") }, { id: "total", label: "Total", get: (r) => formatMs(r.metrics.elapsedMs), best: (rs) => bestBy(rs, (r) => r.metrics.elapsedMs, "min") }, { id: "tps", label: "Speed", get: (r) => formatTps(r.metrics.tokensPerSecond), best: (rs) => bestBy(rs, (r) => r.metrics.tokensPerSecond, "max") }, { id: "in", label: "Input", get: (r) => formatTokens(r.metrics.inputTokens) }, { id: "out", label: "Output", get: (r) => formatTokens(r.metrics.outputTokens) }, ]; if (showCosts) list.push({ id: "cost", label: "Cost", get: (r) => (r.metrics.costUsd === null ? "—" : `${r.metrics.exact ? "" : "≈ "}${formatUsd(r.metrics.costUsd, { precise: r.metrics.costUsd < 0.01 })}`), best: (rs) => bestBy(rs, (r) => r.metrics.costUsd, "min") }); list.push({ id: "votes", label: "Criteria won", get: (r) => (r.criteriaWon.length ? r.criteriaWon.map((id) => criterionById(id, customCriteria).short).join(", ") : "—") }); return list; }, [showCosts, customCriteria]); const bests = React.useMemo(() => Object.fromEntries(cols.map((c) => [c.id, c.best ? c.best(rows) : null])), [cols, rows]); if (!rows.length) return null; return (

Comparison

Best value per column is highlighted. Costs are estimates from list prices unless the provider reports them.

{/* Phone: metric rows, models inline */}
{cols.map((c) => (
{c.label}
{rows.map((r) => (
{c.get(r)}
))}
))}
{/* Desktop: table */}
{cols.map((c) => ( ))} {rows.map((r) => ( {cols.map((c) => ( ))} ))}
Model {c.label}
{r.name} {r.isWinner ? : null} {c.id === "status" ? ( {c.get(r)} ) : ( c.get(r) )}
); } function Identity({ row, size }: { row: ComparisonRow; size: number }) { if (row.hidden) return ; return ; }