SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
6.0 KB · 128 lines tsx
Raw Blame History
1"use client";2import * as React from "react";3import { Trophy } from "lucide-react";4import { ProviderIcon } from "@/components/brand/provider-icon";5import type { LiveMetrics } from "@/lib/arena/metrics";6import { criterionById, type Criterion } from "@/lib/arena/scoring";7import { cn, formatMs, formatTokens, formatUsd } from "@/lib/utils";8import { STATUS_META, StatusDot, formatTps } from "./metrics-strip";9import { BlindAvatar } from "./blind";1011export interface ComparisonRow {12  key: string;13  name: string;14  provider: string | null;15  /** Blind: identity hidden, show letter avatar. */16  hidden?: boolean;17  blindIndex: number;18  metrics: LiveMetrics;19  criteriaWon: string[];20  isWinner: boolean;21}2223interface Props {24  rows: ComparisonRow[];25  showCosts?: boolean;26  customCriteria?: Criterion[];27  className?: string;28}2930type Col = { id: string; label: string; get: (r: ComparisonRow) => string; best?: (rows: ComparisonRow[]) => string | null };3132function bestBy(rows: ComparisonRow[], get: (r: ComparisonRow) => number | null, dir: "min" | "max"): string | null {33  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));34  if (cands.length < 2) return null;35  cands.sort((a, b) => (dir === "min" ? a.v - b.v : b.v - a.v));36  return cands[0].v === cands[1].v ? null : cands[0].key;37}3839/**40 * End-of-run comparison. Table from `md` up; stacked metric rows on phones (never a 4-column table on a phone).41 */42export function ComparisonTable({ rows, showCosts = true, customCriteria = [], className }: Props) {43  const cols = React.useMemo<Col[]>(() => {44    const list: Col[] = [45      { id: "status", label: "Status", get: (r) => STATUS_META[r.metrics.status].label },46      { id: "ttft", label: "TTFT", get: (r) => formatMs(r.metrics.ttftMs), best: (rs) => bestBy(rs, (r) => r.metrics.ttftMs, "min") },47      { id: "total", label: "Total", get: (r) => formatMs(r.metrics.elapsedMs), best: (rs) => bestBy(rs, (r) => r.metrics.elapsedMs, "min") },48      { id: "tps", label: "Speed", get: (r) => formatTps(r.metrics.tokensPerSecond), best: (rs) => bestBy(rs, (r) => r.metrics.tokensPerSecond, "max") },49      { id: "in", label: "Input", get: (r) => formatTokens(r.metrics.inputTokens) },50      { id: "out", label: "Output", get: (r) => formatTokens(r.metrics.outputTokens) },51    ];52    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") });53    list.push({ id: "votes", label: "Criteria won", get: (r) => (r.criteriaWon.length ? r.criteriaWon.map((id) => criterionById(id, customCriteria).short).join(", ") : "—") });54    return list;55  }, [showCosts, customCriteria]);56  const bests = React.useMemo(() => Object.fromEntries(cols.map((c) => [c.id, c.best ? c.best(rows) : null])), [cols, rows]);5758  if (!rows.length) return null;59  return (60    <section className={cn("panel p-3 sm:p-4", className)} aria-label="Comparison">61      <h3 className="text-[13px] font-semibold tracking-tight">Comparison</h3>62      <p className="mt-0.5 text-[12px] text-fg-muted">Best value per column is highlighted. Costs are estimates from list prices unless the provider reports them.</p>6364      {/* Phone: metric rows, models inline */}65      <dl className="mt-3 space-y-2.5 md:hidden">66        {cols.map((c) => (67          <div key={c.id} className="border-t border-hairline pt-2 first:border-t-0 first:pt-0">68            <dt className="text-[10.5px] uppercase tracking-wide text-fg-subtle">{c.label}</dt>69            <dd className="mt-1 grid grid-cols-2 gap-x-3 gap-y-1">70              {rows.map((r) => (71                <div key={r.key} className="flex min-w-0 items-center gap-1.5 text-[12px] tabular-nums">72                  <Identity row={r} size={12} />73                  <span className={cn("truncate", bests[c.id] === r.key ? "font-semibold text-success" : "text-fg")}>{c.get(r)}</span>74                </div>75              ))}76            </dd>77          </div>78        ))}79      </dl>8081      {/* Desktop: table */}82      <div className="mt-3 hidden overflow-x-auto md:block">83        <table className="w-full text-[12.5px] tabular-nums">84          <thead>85            <tr className="text-left text-[10.5px] uppercase tracking-wide text-fg-subtle">86              <th className="pb-2 pr-3 font-medium">Model</th>87              {cols.map((c) => (88                <th key={c.id} className="pb-2 pr-3 font-medium">89                  {c.label}90                </th>91              ))}92            </tr>93          </thead>94          <tbody>95            {rows.map((r) => (96              <tr key={r.key} className="border-t border-hairline">97                <td className="py-2 pr-3">98                  <span className="flex min-w-0 items-center gap-2 font-medium">99                    <Identity row={r} size={14} />100                    <span className="truncate">{r.name}</span>101                    {r.isWinner ? <Trophy className="size-3.5 shrink-0 text-accent" aria-label="Arena winner" /> : null}102                  </span>103                </td>104                {cols.map((c) => (105                  <td key={c.id} className={cn("py-2 pr-3", bests[c.id] === r.key ? "font-semibold text-success" : "text-fg")}>106                    {c.id === "status" ? (107                      <span className="inline-flex items-center gap-1.5">108                        <StatusDot status={r.metrics.status} /> {c.get(r)}109                      </span>110                    ) : (111                      c.get(r)112                    )}113                  </td>114                ))}115              </tr>116            ))}117          </tbody>118        </table>119      </div>120    </section>121  );122}123124function Identity({ row, size }: { row: ComparisonRow; size: number }) {125  if (row.hidden) return <BlindAvatar index={row.blindIndex} size={size + 4} />;126  return <ProviderIcon provider={row.provider} size={size} />;127}128