mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
/benchmarks is now one story: lead, proof, frontier, products, infrastructure, boundaries, methods, then every benchmark and every run on one shelf. A sticky chapter nav marks where you are. "/" opens a palette that jumps to any benchmark; ⌘K stays the site search the header owns. lib/results.ts builds one ledger from the files that hold the figures: - the research runs: every Kai revision of every suite against Jev, the release gate, choice size, joint decisions, latency, validation splits; - the leaderboard snapshot (Enso's boards); - the engine's cells (Zen on the engine against llama.cpp); - the memory, graph and code runs; - the runtime passes. That is 216 results across 35 benchmarks. Each is dated and marked live, frozen or superseded. The one undated row, the sealed board's HarmBench count, says so. The shelf searches and highlights instantly. It is faceted by product, category, metric, measured-by, date, status and result, with counts. Views are kept in the URL, with named saved views, CSV/JSON export, and a compare tray for rows of one benchmark and metric. Every benchmark without a hand-written page gets /benchmarks/<slug>: its history over time, conditions, hardware, source and reproduce command. The sitemap lists these pages. The prebuild now runs scripts/kai.mjs. It pulls the latest validated runs (canonical, public, not inconsistent). The committed snapshot is the frozen fallback when the API cannot be read or answers with fewer runs. The home page's Kai line and /models' family line now read the frozen harness from lib/tally (11 of 12, behind on Jailbreak). v2's 12 of 12 appears only where it is labelled. Type follows the product pages (@hanzo/ui/marketing Display, Title, Eyebrow, Lede). Motion is gui transitions, with no entrance that ships hidden; the design system zeroes durations under reduced motion. The board and shelf are imported only by the index, so the other benchmark pages stay under budget. /benchmarks has its own budget, 440 KB. Co-authored-by: Hanzo Dev <[email protected]>
105 lines
4.7 KiB
TypeScript
105 lines
4.7 KiB
TypeScript
import type { ReactNode } from 'react'
|
||
import { Cell, Table, Td, Th, Tr } from './parts'
|
||
|
||
export * from './parts'
|
||
|
||
/**
|
||
* The furniture every bench page is built from.
|
||
*
|
||
* One table renderer, not four. The published runs come in four shapes —
|
||
* retrieval rows, code rows, the per-category sections, the LongMemEval tally —
|
||
* and each was a table component of its own that differed only in which columns
|
||
* it spelled. A column is `{ head, get }`; the shape it reads is the caller's
|
||
* business, and the borders, the alignment, the tabular figures and the
|
||
* horizontal scroll are settled once here.
|
||
*
|
||
* The index is built from the same pieces, so a reader arriving from it does
|
||
* not land on a page that looks like a different site — and so the index holds
|
||
* no style value of its own, which keeps its source free of every digit that is
|
||
* not a figure read from the data.
|
||
*
|
||
* Every piece is a component on gui props. These pages are dark by design — a
|
||
* black page in white type whatever the site's theme — so the inks are stated
|
||
* against that ground rather than the theme's.
|
||
*/
|
||
|
||
export const n0 = (v: number | null | undefined) => (v == null ? '–' : v.toFixed(0))
|
||
export const n1 = (v: number | null | undefined) => (v == null ? '–' : v.toFixed(1))
|
||
export const ms = (v: number | null | undefined) => (v == null ? '–' : v.toFixed(2))
|
||
|
||
export type Interval = { mean: number; lo: number; hi: number }
|
||
export type Metric = Interval | number | null
|
||
export const val = (m: Metric | undefined) => (m == null ? null : typeof m === 'number' ? m : m.mean)
|
||
/** A rate as a percentage to one place, the precision every metric here carries. */
|
||
export const F = (m: Metric | undefined) => { const v = val(m); return v == null ? '–' : (v * 100).toFixed(1) }
|
||
/** The same to two places, for a rate near one hundred where one place hides the difference. */
|
||
export const F2 = (m: Metric | undefined) => { const v = val(m); return v == null ? '–' : (v * 100).toFixed(2) }
|
||
/** A rate as a percentage, for a bar. */
|
||
export const pc = (m: Metric | undefined) => { const v = val(m); return v == null ? null : v * 100 }
|
||
/** A duration in milliseconds at the precision its size needs: 0.063 ms, 1.4 ms, 5,357 ms. */
|
||
export const elapsed = (v: number | null | undefined) =>
|
||
v == null ? '–' : v >= 100 ? `${Math.round(v).toLocaleString('en-US')} ms` : `${v.toFixed(v < 1 ? 3 : 1)} ms`
|
||
/** Names joined as a sentence joins them: `a, b, and c`. */
|
||
export const list = (xs: readonly string[]) => new Intl.ListFormat('en').format(xs)
|
||
|
||
/**
|
||
* A metric's column head, in the one place it is spelled.
|
||
*
|
||
* The run files carry the key a scorer wrote (`substring_em`, `ndcg@20`); a
|
||
* reader needs the name the benchmark's own paper uses. Two pages spelling
|
||
* `sem` differently would be two benchmarks as far as anyone reading them is
|
||
* concerned.
|
||
*/
|
||
export const METRIC: Record<string, string> = {
|
||
of: 'in set', 'all@5': 'ALL@5', 'all@10': 'ALL@10', 'all@20': 'ALL@20', 'any@20': 'ANY@20',
|
||
'recall@10': 'R@10', mrr: 'MRR', 'ndcg@20': 'nDCG@20', supported: 'supported',
|
||
f1: 'F1', em: 'EM', substring_em: 'substring EM', sem: 'substring EM',
|
||
recall: 'recall', all: 'ALL', any: 'ANY',
|
||
examined: 'candidates', tokens: 'tokens/q', p50: 'p50 ms', p95: 'p95 ms',
|
||
}
|
||
|
||
/** Which metrics a section table shows, and in what order. Absent keys are dropped. */
|
||
export const SHOW = ['of', 'all@20', 'any@20', 'recall@10', 'recall', 'all', 'any', 'mrr', 'ndcg@20', 'supported', 'f1', 'em', 'substring_em', 'sem', 'examined', 'tokens', 'p50', 'p95']
|
||
|
||
/** Counts stay counts, milliseconds stay milliseconds, rates become percentages. */
|
||
export function metric(v: Metric | undefined, key: string): ReactNode {
|
||
if (v == null) return '–'
|
||
if (typeof v === 'number') {
|
||
if (key === 'examined' || key === 'tokens' || key === 'of') return v.toFixed(0)
|
||
if (key.startsWith('p')) return v.toFixed(2)
|
||
return (v * 100).toFixed(1)
|
||
}
|
||
return <Cell m={v} />
|
||
}
|
||
|
||
export type Col<R> = { head: ReactNode; get: (r: R) => ReactNode; left?: boolean }
|
||
|
||
/** The one table. Columns describe themselves; the rows are whatever shape the run file has. */
|
||
export function Grid<R>({ cols, rows, keyOf, dim }: { cols: Col<R>[]; rows: R[]; keyOf: (r: R) => string; dim?: (r: R) => boolean }) {
|
||
return (
|
||
<Table>
|
||
<thead>
|
||
<tr>
|
||
{cols.map((c, i) => (
|
||
<Th key={i} left={c.left}>
|
||
{c.head}
|
||
</Th>
|
||
))}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{rows.map((r) => (
|
||
<Tr key={keyOf(r)} dim={dim?.(r)}>
|
||
{cols.map((c, i) => (
|
||
<Td key={i} left={c.left}>
|
||
{c.get(r)}
|
||
</Td>
|
||
))}
|
||
</Tr>
|
||
))}
|
||
</tbody>
|
||
</Table>
|
||
)
|
||
}
|
||
|