"use client";
import { useMemo, useState } from "react";
import { cn } from "@/lib/utils";
import { hasBench, ms, pct, providerTone, usd, type ModelRow } from "./format";
/** Models × benchmark categories. Sequential single-hue fill (accent) by pass rate; judged score printed when present. */
export function Heatmap({ models, categories }: { models: ModelRow[]; categories: { id: string; label: string }[] }) {
const rows = useMemo(() => models.filter(hasBench).sort((a, b) => (b.benchmark!.mean_score ?? 0) - (a.benchmark!.mean_score ?? 0) || (a.benchmark!.total_median_ms ?? 0) - (b.benchmark!.total_median_ms ?? 0)), [models]);
const [hover, setHover] = useState<{ m: ModelRow; c: string; x: number; y: number } | null>(null);
const fill = (p: number | null | undefined) => (p == null ? "var(--bg-sunk)" : `color-mix(in oklab, var(--accent) ${Math.round(6 + p * p * 52)}%, var(--bg-elev))`);
const cell = hover ? hover.m.benchmark?.by_category?.[hover.c] : null;
return (
| Model | {categories.map((c) => {c.label} | )}
{rows.map((m) =>
| {m.id} |
{categories.map((c) => { const v = m.benchmark?.by_category?.[c.id]; return | ; })}
)}
Fill = pass rate{[0, 0.5, 0.75, 0.9, 1].map((p) => )}0 → 100 %Number = LLM-judge score (1–5) where a judged rubric existsGrey = not run
{hover &&
{hover.m.id}
{categories.find((c) => c.id === hover.c)?.label}
{cell ?
- Pass
- {pct(cell.pass_rate)} ({cell.runs} runs)
- Score
- {cell.score.toFixed(2)}
{cell.judged != null && <>- Judged
- {cell.judged.toFixed(2)} / 5
>}- TTFT
- {ms(cell.ttft_ms)}
- Latency
- {ms(cell.total_ms)}
- tok/s
- {Math.round(cell.tps)}
- $/run
- {usd(cell.cost_mean)}
:
Not run for this model.
}
}
);
}