Spaces:
Running
Restore curated benchmark families; polish frontier panel UX
Browse files* clean-hierarchy: stop nuking curated benchmark families whose rows
happen to all be republished by a single source. The strict-subset
wrapper rule and the consolidate-poorer-duplicate pass were both
treating "tau2-bench has all its benches under exgentic-open-agent"
as a redundant wrapper — but tau2-bench is a real curated leaderboard,
not a duplicate. Two changes:
- the strict-subset rule now requires the two family keys to be
textually related (slug equality or substring) before declaring
one a wrapper of the other. live-bench still absorbs livebench
(their slugs match); tau2-bench is no longer absorbed by
exgentic-open-agent (`tau2bench` vs `exgenticopenagent` — no
relationship).
- isPoorerDuplicate now also requires the two bench instances to
share an eval_summary_id before considering them duplicates.
gaia%2Fgaia and hal%2Fgaia are different physical rows reporting
the same benchmark concept; both should surface. live-bench's
`live-bench%2Flivebench-coding` is literally the same row as
livebench's, so the dedup still fires there.
Net effect: family count goes from 50 to 64, restoring tau-bench,
tau2-bench, gaia, judgebench, mmlu, mmlu-pro, mathvista, mathverse,
videomme, appworld, arena, terminal-bench-2-0 etc. as their own
cards on /evals.
* score-distribution: reorganise the panel header into two stacked
rows so the View toggle (Distribution / Frontier) and the Metric
picker chips read as visually distinct controls — same chip styling
was making them blur into one undifferentiated strip. Each row now
carries its own subdued kicker label ("View", "Metric"). Add a
getMetricChipLabel helper that walks display_name → metric_name →
metric_id → column_key tail and humanises underscores so AgentHarm's
null-display-name metrics surface as "avg full score", "avg refusals"
etc. instead of four chips literally labelled "Metric".
* score-distribution: render the Frontier plot in HTML over an
unstretched 0-100 SVG so the dots stay round and the year labels
stay readable. preserveAspectRatio="none" was squashing the
circles into ovals and the glyphs into illegible bars.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- components/eval-detail.tsx +61 -17
- components/score-distribution.tsx +511 -23
- lib/clean-hierarchy.ts +48 -10
|
@@ -463,19 +463,46 @@ function describeLeaderboardMetric(metric: LeaderboardMetric) {
|
|
| 463 |
return metric.canonical_display_name || metric.display_name
|
| 464 |
}
|
| 465 |
|
| 466 |
-
function
|
| 467 |
-
if (!value) {
|
| 468 |
-
return "Metric"
|
| 469 |
-
}
|
| 470 |
-
|
| 471 |
const parts = value
|
| 472 |
.split("/")
|
| 473 |
.map((part) => part.trim())
|
| 474 |
.filter(Boolean)
|
| 475 |
-
|
| 476 |
return parts[parts.length - 1] ?? value
|
| 477 |
}
|
| 478 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 479 |
/**
|
| 480 |
* Best-effort "setup" caption for a row (e.g. "8-shot CoT", "0-shot").
|
| 481 |
*
|
|
@@ -1078,14 +1105,23 @@ export function EvalDetail({
|
|
| 1078 |
/>
|
| 1079 |
)}
|
| 1080 |
|
| 1081 |
-
{/* Score distribution — paper-themed mean/median/quartile summary
|
|
|
|
| 1082 |
{leaderboardRows.length >= 3 && (
|
| 1083 |
<div className="mb-4">
|
| 1084 |
<ScoreDistribution
|
| 1085 |
-
|
| 1086 |
-
|
| 1087 |
-
|
| 1088 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1089 |
/>
|
| 1090 |
</div>
|
| 1091 |
)}
|
|
@@ -2017,18 +2053,26 @@ function MultiMetricLeaderboard({
|
|
| 2017 |
{(() => {
|
| 2018 |
const distSeries = visibleMetrics
|
| 2019 |
.map((metric) => {
|
| 2020 |
-
const
|
| 2021 |
-
|
| 2022 |
-
|
| 2023 |
-
|
| 2024 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2025 |
return {
|
| 2026 |
key: metric.column_key,
|
| 2027 |
label,
|
| 2028 |
caption: metric.unit ?? undefined,
|
| 2029 |
-
values,
|
| 2030 |
unit: metric.unit ?? undefined,
|
| 2031 |
lowerIsBetter: metric.lower_is_better,
|
|
|
|
| 2032 |
}
|
| 2033 |
})
|
| 2034 |
.filter((entry): entry is NonNullable<typeof entry> => entry !== null)
|
|
|
|
| 463 |
return metric.canonical_display_name || metric.display_name
|
| 464 |
}
|
| 465 |
|
| 466 |
+
function compactizePath(value: string): string {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 467 |
const parts = value
|
| 468 |
.split("/")
|
| 469 |
.map((part) => part.trim())
|
| 470 |
.filter(Boolean)
|
|
|
|
| 471 |
return parts[parts.length - 1] ?? value
|
| 472 |
}
|
| 473 |
|
| 474 |
+
function getCompactMetricLabel(value: string | undefined): string {
|
| 475 |
+
if (value && value.trim()) return compactizePath(value)
|
| 476 |
+
return "Metric"
|
| 477 |
+
}
|
| 478 |
+
|
| 479 |
+
/**
|
| 480 |
+
* Build a chip-friendly label for a leaderboard metric. Prefers
|
| 481 |
+
* display_name, then metric_name, then a humanised tail of metric_id /
|
| 482 |
+
* column_key. The upstream pipeline frequently leaves display_name
|
| 483 |
+
* blank (e.g. inspect_evals/avg_full_score), in which case the
|
| 484 |
+
* column_key tail ('avg_full_score') is what we want to surface.
|
| 485 |
+
*/
|
| 486 |
+
function getMetricChipLabel(metric: {
|
| 487 |
+
display_name?: string | null
|
| 488 |
+
metric_name?: string | null
|
| 489 |
+
metric_id?: string | null
|
| 490 |
+
column_key?: string | null
|
| 491 |
+
}): string {
|
| 492 |
+
const candidates = [
|
| 493 |
+
metric.display_name,
|
| 494 |
+
metric.metric_name,
|
| 495 |
+
metric.metric_id,
|
| 496 |
+
metric.column_key,
|
| 497 |
+
]
|
| 498 |
+
for (const c of candidates) {
|
| 499 |
+
if (c && String(c).trim()) {
|
| 500 |
+
return compactizePath(String(c)).replace(/_/g, " ")
|
| 501 |
+
}
|
| 502 |
+
}
|
| 503 |
+
return "Metric"
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
/**
|
| 507 |
* Best-effort "setup" caption for a row (e.g. "8-shot CoT", "0-shot").
|
| 508 |
*
|
|
|
|
| 1105 |
/>
|
| 1106 |
)}
|
| 1107 |
|
| 1108 |
+
{/* Score distribution — paper-themed mean/median/quartile summary,
|
| 1109 |
+
with an optional Frontier toggle when models carry release dates. */}
|
| 1110 |
{leaderboardRows.length >= 3 && (
|
| 1111 |
<div className="mb-4">
|
| 1112 |
<ScoreDistribution
|
| 1113 |
+
series={[{
|
| 1114 |
+
key: "primary",
|
| 1115 |
+
label: lb.metric_config.unit ?? "Score",
|
| 1116 |
+
values: leaderboardRows.map((r) => r.modelResult.score),
|
| 1117 |
+
unit: lb.metric_config.unit,
|
| 1118 |
+
lowerIsBetter: lb.metric_config.lower_is_better,
|
| 1119 |
+
points: leaderboardRows.map((r) => ({
|
| 1120 |
+
score: r.modelResult.score,
|
| 1121 |
+
releaseDate: r.modelResult.model_info.release_date,
|
| 1122 |
+
modelName: r.modelResult.model_info.name,
|
| 1123 |
+
})),
|
| 1124 |
+
}]}
|
| 1125 |
/>
|
| 1126 |
</div>
|
| 1127 |
)}
|
|
|
|
| 2053 |
{(() => {
|
| 2054 |
const distSeries = visibleMetrics
|
| 2055 |
.map((metric) => {
|
| 2056 |
+
const points: Array<{ score: number; releaseDate: string | null; modelName: string }> = []
|
| 2057 |
+
for (const r of filteredRows) {
|
| 2058 |
+
const score = r.values[metric.column_key]
|
| 2059 |
+
if (!isNumericScore(score)) continue
|
| 2060 |
+
points.push({
|
| 2061 |
+
score,
|
| 2062 |
+
releaseDate: r.model_info?.release_date ?? null,
|
| 2063 |
+
modelName: r.model_info?.name ?? "",
|
| 2064 |
+
})
|
| 2065 |
+
}
|
| 2066 |
+
if (points.length < 3) return null
|
| 2067 |
+
const label = getMetricChipLabel(metric)
|
| 2068 |
return {
|
| 2069 |
key: metric.column_key,
|
| 2070 |
label,
|
| 2071 |
caption: metric.unit ?? undefined,
|
| 2072 |
+
values: points.map((p) => p.score),
|
| 2073 |
unit: metric.unit ?? undefined,
|
| 2074 |
lowerIsBetter: metric.lower_is_better,
|
| 2075 |
+
points,
|
| 2076 |
}
|
| 2077 |
})
|
| 2078 |
.filter((entry): entry is NonNullable<typeof entry> => entry !== null)
|
|
@@ -12,6 +12,17 @@ interface ScoreSeries {
|
|
| 12 |
values: number[]
|
| 13 |
unit?: string
|
| 14 |
lowerIsBetter?: boolean
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
}
|
| 16 |
|
| 17 |
interface ScoreDistributionProps {
|
|
@@ -38,6 +49,27 @@ interface SummaryStats {
|
|
| 38 |
q3: number
|
| 39 |
}
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
function computeStats(values: number[]): SummaryStats | null {
|
| 42 |
const sorted = values.filter((v) => Number.isFinite(v)).slice().sort((a, b) => a - b)
|
| 43 |
const n = sorted.length
|
|
@@ -116,6 +148,45 @@ export function ScoreDistribution({
|
|
| 116 |
|
| 117 |
const stats = useMemo(() => (active ? computeStats(active.values) : null), [active])
|
| 118 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
const density = useMemo(() => {
|
| 120 |
if (!active || !stats) return null
|
| 121 |
if (stats.max === stats.min) {
|
|
@@ -199,15 +270,58 @@ export function ScoreDistribution({
|
|
| 199 |
}}
|
| 200 |
>
|
| 201 |
{!compact && (
|
| 202 |
-
<div className="
|
| 203 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
<div
|
| 205 |
className="font-mono uppercase shrink-0"
|
| 206 |
-
style={{ fontSize:
|
| 207 |
>
|
| 208 |
-
|
| 209 |
</div>
|
| 210 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
<div className="flex flex-wrap items-center gap-1.5">
|
| 212 |
{seriesList.map((s) => {
|
| 213 |
const on = s.key === active.key
|
|
@@ -232,22 +346,17 @@ export function ScoreDistribution({
|
|
| 232 |
)
|
| 233 |
})}
|
| 234 |
</div>
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
className="font-mono uppercase shrink-0"
|
| 247 |
-
style={{ fontSize: 9.5, letterSpacing: "0.12em", color: "var(--fg-subtle)" }}
|
| 248 |
-
>
|
| 249 |
-
{directionHint}
|
| 250 |
-
</div>
|
| 251 |
</div>
|
| 252 |
)}
|
| 253 |
|
|
@@ -261,6 +370,15 @@ export function ScoreDistribution({
|
|
| 261 |
</div>
|
| 262 |
)}
|
| 263 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
<svg
|
| 265 |
viewBox={`0 0 ${width} ${plotHeight + 8}`}
|
| 266 |
preserveAspectRatio="none"
|
|
@@ -349,8 +467,11 @@ export function ScoreDistribution({
|
|
| 349 |
vectorEffect="non-scaling-stroke"
|
| 350 |
/>
|
| 351 |
</svg>
|
|
|
|
| 352 |
|
| 353 |
-
{/* Caption row
|
|
|
|
|
|
|
| 354 |
<div
|
| 355 |
className="mt-2 flex flex-wrap items-baseline font-mono"
|
| 356 |
style={{
|
|
@@ -389,8 +510,9 @@ export function ScoreDistribution({
|
|
| 389 |
</span>
|
| 390 |
))}
|
| 391 |
</div>
|
|
|
|
| 392 |
|
| 393 |
-
{!compact && (
|
| 394 |
<div
|
| 395 |
className="mt-1 flex items-center gap-3 font-mono"
|
| 396 |
style={{ fontSize: 9, letterSpacing: "0.06em", color: "var(--fg-subtle)" }}
|
|
@@ -436,3 +558,369 @@ export function ScoreDistribution({
|
|
| 436 |
</div>
|
| 437 |
)
|
| 438 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
values: number[]
|
| 13 |
unit?: string
|
| 14 |
lowerIsBetter?: boolean
|
| 15 |
+
/**
|
| 16 |
+
* Per-model rows for the optional frontier-plot view. When provided
|
| 17 |
+
* (and at least one row carries a parseable releaseDate), the panel
|
| 18 |
+
* exposes a chip toggle that swaps the density curve for a
|
| 19 |
+
* release-date frontier (cumulative best score over time).
|
| 20 |
+
*/
|
| 21 |
+
points?: Array<{
|
| 22 |
+
score: number
|
| 23 |
+
releaseDate?: string | null
|
| 24 |
+
modelName?: string | null
|
| 25 |
+
}>
|
| 26 |
}
|
| 27 |
|
| 28 |
interface ScoreDistributionProps {
|
|
|
|
| 49 |
q3: number
|
| 50 |
}
|
| 51 |
|
| 52 |
+
function parseReleaseDate(value: string | null | undefined): number | null {
|
| 53 |
+
if (!value) return null
|
| 54 |
+
const raw = String(value).trim()
|
| 55 |
+
if (!raw) return null
|
| 56 |
+
// Numeric epoch — treat seconds-since-epoch values as such, ms otherwise.
|
| 57 |
+
const numeric = Number(raw)
|
| 58 |
+
if (!Number.isNaN(numeric) && !raw.includes("-")) {
|
| 59 |
+
const ms = numeric > 1_000_000_000_000 ? numeric : numeric * 1000
|
| 60 |
+
return Number.isFinite(ms) ? ms : null
|
| 61 |
+
}
|
| 62 |
+
const parsed = new Date(raw).getTime()
|
| 63 |
+
return Number.isFinite(parsed) ? parsed : null
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
const MONTH_LABELS = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]
|
| 67 |
+
function formatMonthYear(ms: number): string {
|
| 68 |
+
const d = new Date(ms)
|
| 69 |
+
if (Number.isNaN(d.getTime())) return ""
|
| 70 |
+
return `${MONTH_LABELS[d.getUTCMonth()]} ${d.getUTCFullYear()}`
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
function computeStats(values: number[]): SummaryStats | null {
|
| 74 |
const sorted = values.filter((v) => Number.isFinite(v)).slice().sort((a, b) => a - b)
|
| 75 |
const n = sorted.length
|
|
|
|
| 148 |
|
| 149 |
const stats = useMemo(() => (active ? computeStats(active.values) : null), [active])
|
| 150 |
|
| 151 |
+
// Frontier-plot data: parse release dates, sort by time, then walk the
|
| 152 |
+
// sequence emitting an event whenever a model improves on the best
|
| 153 |
+
// score seen so far. Honours lowerIsBetter so e.g. "Mean Response
|
| 154 |
+
// Time · ms" shows the frontier descending instead of climbing.
|
| 155 |
+
const frontier = useMemo(() => {
|
| 156 |
+
if (!active?.points || active.points.length === 0) return null
|
| 157 |
+
const lowerIsBetter = active.lowerIsBetter ?? false
|
| 158 |
+
const parsed = active.points
|
| 159 |
+
.map((p) => {
|
| 160 |
+
const t = parseReleaseDate(p.releaseDate)
|
| 161 |
+
if (t == null) return null
|
| 162 |
+
if (!Number.isFinite(p.score)) return null
|
| 163 |
+
return { time: t, score: p.score, name: p.modelName ?? "" }
|
| 164 |
+
})
|
| 165 |
+
.filter((p): p is { time: number; score: number; name: string } => p !== null)
|
| 166 |
+
.sort((a, b) => a.time - b.time)
|
| 167 |
+
|
| 168 |
+
if (parsed.length < 2) return null
|
| 169 |
+
|
| 170 |
+
let best = lowerIsBetter ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY
|
| 171 |
+
const events: typeof parsed = []
|
| 172 |
+
for (const p of parsed) {
|
| 173 |
+
const better = lowerIsBetter ? p.score < best : p.score > best
|
| 174 |
+
if (better) {
|
| 175 |
+
best = p.score
|
| 176 |
+
events.push(p)
|
| 177 |
+
}
|
| 178 |
+
}
|
| 179 |
+
if (events.length < 2) return null
|
| 180 |
+
return { events, samples: parsed }
|
| 181 |
+
}, [active])
|
| 182 |
+
|
| 183 |
+
const canShowFrontier = frontier != null
|
| 184 |
+
const [view, setView] = useState<"distribution" | "frontier">("distribution")
|
| 185 |
+
// If the active series doesn't support frontier (e.g. user switched to
|
| 186 |
+
// a metric whose models don't carry release_date), fall back to the
|
| 187 |
+
// distribution view rather than rendering an empty panel.
|
| 188 |
+
const effectiveView = canShowFrontier ? view : "distribution"
|
| 189 |
+
|
| 190 |
const density = useMemo(() => {
|
| 191 |
if (!active || !stats) return null
|
| 192 |
if (stats.max === stats.min) {
|
|
|
|
| 270 |
}}
|
| 271 |
>
|
| 272 |
{!compact && (
|
| 273 |
+
<div className="mb-3 space-y-2">
|
| 274 |
+
{/* Row 1 — View toggle (left) + direction hint (right). The
|
| 275 |
+
kicker label makes it clear that these chips switch the
|
| 276 |
+
chart type, distinguishing them from the metric chips
|
| 277 |
+
below. */}
|
| 278 |
+
<div className="flex items-center justify-between gap-3 flex-wrap">
|
| 279 |
+
<div className="flex items-center gap-2 min-w-0">
|
| 280 |
+
<span
|
| 281 |
+
className="font-mono uppercase shrink-0"
|
| 282 |
+
style={{ fontSize: 10, letterSpacing: "0.14em", color: "var(--fg-subtle)" }}
|
| 283 |
+
>
|
| 284 |
+
{canShowFrontier ? "View" : "Score distribution"}
|
| 285 |
+
</span>
|
| 286 |
+
{canShowFrontier && (
|
| 287 |
+
<div className="flex items-center gap-1">
|
| 288 |
+
<button
|
| 289 |
+
type="button"
|
| 290 |
+
className={`ec-pill${effectiveView === "distribution" ? " on" : ""}`}
|
| 291 |
+
onClick={() => setView("distribution")}
|
| 292 |
+
>
|
| 293 |
+
Distribution
|
| 294 |
+
</button>
|
| 295 |
+
<button
|
| 296 |
+
type="button"
|
| 297 |
+
className={`ec-pill${effectiveView === "frontier" ? " on" : ""}`}
|
| 298 |
+
onClick={() => setView("frontier")}
|
| 299 |
+
title="Frontier score over model release dates (cumulative best)."
|
| 300 |
+
>
|
| 301 |
+
Frontier
|
| 302 |
+
</button>
|
| 303 |
+
</div>
|
| 304 |
+
)}
|
| 305 |
+
</div>
|
| 306 |
<div
|
| 307 |
className="font-mono uppercase shrink-0"
|
| 308 |
+
style={{ fontSize: 9.5, letterSpacing: "0.12em", color: "var(--fg-subtle)" }}
|
| 309 |
>
|
| 310 |
+
{directionHint}
|
| 311 |
</div>
|
| 312 |
+
</div>
|
| 313 |
+
|
| 314 |
+
{/* Row 2 — Metric chips. Only shown when there's more than
|
| 315 |
+
one series; otherwise the active label gets a quiet inline
|
| 316 |
+
caption next to the view kicker. */}
|
| 317 |
+
{showPicker ? (
|
| 318 |
+
<div className="flex items-baseline gap-2 flex-wrap">
|
| 319 |
+
<span
|
| 320 |
+
className="font-mono uppercase shrink-0"
|
| 321 |
+
style={{ fontSize: 10, letterSpacing: "0.14em", color: "var(--fg-subtle)" }}
|
| 322 |
+
>
|
| 323 |
+
Metric
|
| 324 |
+
</span>
|
| 325 |
<div className="flex flex-wrap items-center gap-1.5">
|
| 326 |
{seriesList.map((s) => {
|
| 327 |
const on = s.key === active.key
|
|
|
|
| 346 |
)
|
| 347 |
})}
|
| 348 |
</div>
|
| 349 |
+
</div>
|
| 350 |
+
) : (
|
| 351 |
+
<div
|
| 352 |
+
className="font-mono uppercase truncate"
|
| 353 |
+
style={{ fontSize: 11, letterSpacing: "0.08em", color: "var(--fg-muted)" }}
|
| 354 |
+
title={active.label}
|
| 355 |
+
>
|
| 356 |
+
{active.label}
|
| 357 |
+
{active.unit ? <span style={{ color: "var(--fg-subtle)" }}>{" · " + active.unit}</span> : null}
|
| 358 |
+
</div>
|
| 359 |
+
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
</div>
|
| 361 |
)}
|
| 362 |
|
|
|
|
| 370 |
</div>
|
| 371 |
)}
|
| 372 |
|
| 373 |
+
{effectiveView === "frontier" && frontier ? (
|
| 374 |
+
<FrontierPlot
|
| 375 |
+
events={frontier.events}
|
| 376 |
+
samples={frontier.samples}
|
| 377 |
+
unit={active.unit}
|
| 378 |
+
lowerIsBetter={active.lowerIsBetter ?? false}
|
| 379 |
+
label={active.label}
|
| 380 |
+
/>
|
| 381 |
+
) : (
|
| 382 |
<svg
|
| 383 |
viewBox={`0 0 ${width} ${plotHeight + 8}`}
|
| 384 |
preserveAspectRatio="none"
|
|
|
|
| 467 |
vectorEffect="non-scaling-stroke"
|
| 468 |
/>
|
| 469 |
</svg>
|
| 470 |
+
)}
|
| 471 |
|
| 472 |
+
{/* Caption row — hidden in frontier view since it tracks
|
| 473 |
+
distribution stats; the frontier panel renders its own caption. */}
|
| 474 |
+
{effectiveView !== "frontier" && (
|
| 475 |
<div
|
| 476 |
className="mt-2 flex flex-wrap items-baseline font-mono"
|
| 477 |
style={{
|
|
|
|
| 510 |
</span>
|
| 511 |
))}
|
| 512 |
</div>
|
| 513 |
+
)}
|
| 514 |
|
| 515 |
+
{!compact && effectiveView !== "frontier" && (
|
| 516 |
<div
|
| 517 |
className="mt-1 flex items-center gap-3 font-mono"
|
| 518 |
style={{ fontSize: 9, letterSpacing: "0.06em", color: "var(--fg-subtle)" }}
|
|
|
|
| 558 |
</div>
|
| 559 |
)
|
| 560 |
}
|
| 561 |
+
|
| 562 |
+
interface FrontierPlotProps {
|
| 563 |
+
/** Strictly-improving subset of the input — each entry pushes the
|
| 564 |
+
* cumulative best score further. Already sorted ascending by time. */
|
| 565 |
+
events: Array<{ time: number; score: number; name: string }>
|
| 566 |
+
/** Every dated sample (improving or not), used as background dots. */
|
| 567 |
+
samples: Array<{ time: number; score: number; name: string }>
|
| 568 |
+
unit?: string
|
| 569 |
+
lowerIsBetter: boolean
|
| 570 |
+
label: string
|
| 571 |
+
}
|
| 572 |
+
|
| 573 |
+
function FrontierPlot({ events, samples, unit, lowerIsBetter, label }: FrontierPlotProps) {
|
| 574 |
+
const PLOT_HEIGHT = 180
|
| 575 |
+
const PAD_T = 8
|
| 576 |
+
const PAD_B = 22 // room for year labels under the axis
|
| 577 |
+
const PAD_L_PCT = 1
|
| 578 |
+
const PAD_R_PCT = 1
|
| 579 |
+
|
| 580 |
+
const tMin = Math.min(...samples.map((s) => s.time))
|
| 581 |
+
const tMaxData = Math.max(...samples.map((s) => s.time))
|
| 582 |
+
// Always extend the rightmost edge to "now" so the user sees how
|
| 583 |
+
// long the current frontier holder has been on top.
|
| 584 |
+
const tMax = Math.max(tMaxData, Date.now())
|
| 585 |
+
const tRange = tMax - tMin || 1
|
| 586 |
+
const sValues = samples.map((s) => s.score)
|
| 587 |
+
const sMin = Math.min(...sValues)
|
| 588 |
+
const sMax = Math.max(...sValues)
|
| 589 |
+
const sRange = sMax - sMin || Math.abs(sMax) || 1
|
| 590 |
+
// Pad y so dots don't kiss the borders.
|
| 591 |
+
const yLo = sMin - sRange * 0.05
|
| 592 |
+
const yHi = sMax + sRange * 0.05
|
| 593 |
+
const yRange = yHi - yLo || 1
|
| 594 |
+
|
| 595 |
+
// Percent helpers — used for both HTML overlay positioning and the
|
| 596 |
+
// SVG path (which uses a 0-100 viewBox so the line scales with the
|
| 597 |
+
// container without distorting other glyphs).
|
| 598 |
+
const xPct = (t: number) =>
|
| 599 |
+
PAD_L_PCT + ((t - tMin) / tRange) * (100 - PAD_L_PCT - PAD_R_PCT)
|
| 600 |
+
const yPct = (s: number) =>
|
| 601 |
+
100 - ((s - yLo) / yRange) * 100 // 0 at top, 100 at bottom
|
| 602 |
+
|
| 603 |
+
// Pixel helpers for the step-line SVG. Keep its viewBox at 100x100
|
| 604 |
+
// so it overlays the container 1:1, while strokeWidth uses
|
| 605 |
+
// vectorEffect=non-scaling-stroke so the line stays crisp.
|
| 606 |
+
let d = ""
|
| 607 |
+
for (let i = 0; i < events.length; i++) {
|
| 608 |
+
const e = events[i]
|
| 609 |
+
const x = xPct(e.time)
|
| 610 |
+
const y = yPct(e.score)
|
| 611 |
+
if (i === 0) {
|
| 612 |
+
d += `M${x.toFixed(3)},${y.toFixed(3)} `
|
| 613 |
+
} else {
|
| 614 |
+
const prev = events[i - 1]
|
| 615 |
+
const yPrev = yPct(prev.score)
|
| 616 |
+
d += `L${x.toFixed(3)},${yPrev.toFixed(3)} L${x.toFixed(3)},${y.toFixed(3)} `
|
| 617 |
+
}
|
| 618 |
+
}
|
| 619 |
+
if (events.length > 0) {
|
| 620 |
+
const last = events[events.length - 1]
|
| 621 |
+
d += `L${xPct(tMax).toFixed(3)},${yPct(last.score).toFixed(3)}`
|
| 622 |
+
}
|
| 623 |
+
|
| 624 |
+
// Year tick marks along the x-axis. Keep at most 6 to avoid label
|
| 625 |
+
// collisions on narrow viewports.
|
| 626 |
+
const startYear = new Date(tMin).getUTCFullYear()
|
| 627 |
+
const endYear = new Date(tMax).getUTCFullYear()
|
| 628 |
+
const yearSpan = endYear - startYear
|
| 629 |
+
const tickStep = yearSpan <= 6 ? 1 : Math.ceil(yearSpan / 6)
|
| 630 |
+
const yearTicks: number[] = []
|
| 631 |
+
for (let y = startYear; y <= endYear; y += tickStep) yearTicks.push(y)
|
| 632 |
+
|
| 633 |
+
// Pre-bucket samples that *aren't* on the frontier so we don't
|
| 634 |
+
// double-render them (the frontier dots are emphasised separately).
|
| 635 |
+
const eventTimes = new Set(events.map((e) => `${e.time}|${e.score}`))
|
| 636 |
+
const bgSamples = samples.filter((s) => !eventTimes.has(`${s.time}|${s.score}`))
|
| 637 |
+
|
| 638 |
+
// Local hover state so we can render a richer label than the native
|
| 639 |
+
// `title=` tooltip — keeps the dot and the popup nameplate in sync
|
| 640 |
+
// even when the cursor sits right between two dots.
|
| 641 |
+
const [hover, setHover] = useState<{
|
| 642 |
+
x: number
|
| 643 |
+
y: number
|
| 644 |
+
name: string
|
| 645 |
+
when: string
|
| 646 |
+
score: string
|
| 647 |
+
onFrontier: boolean
|
| 648 |
+
} | null>(null)
|
| 649 |
+
|
| 650 |
+
return (
|
| 651 |
+
<div>
|
| 652 |
+
<div
|
| 653 |
+
style={{
|
| 654 |
+
position: "relative",
|
| 655 |
+
width: "100%",
|
| 656 |
+
height: PLOT_HEIGHT,
|
| 657 |
+
paddingTop: PAD_T,
|
| 658 |
+
paddingBottom: PAD_B,
|
| 659 |
+
boxSizing: "border-box",
|
| 660 |
+
}}
|
| 661 |
+
onMouseLeave={() => setHover(null)}
|
| 662 |
+
>
|
| 663 |
+
{/* Inner plot canvas (the area minus axis padding). */}
|
| 664 |
+
<div
|
| 665 |
+
style={{
|
| 666 |
+
position: "absolute",
|
| 667 |
+
top: PAD_T,
|
| 668 |
+
bottom: PAD_B,
|
| 669 |
+
left: 0,
|
| 670 |
+
right: 0,
|
| 671 |
+
}}
|
| 672 |
+
>
|
| 673 |
+
{/* Step line. The SVG uses a 0-100 viewBox so its path lines
|
| 674 |
+
up with HTML overlays positioned via the same xPct/yPct
|
| 675 |
+
helpers; non-scaling-stroke keeps the stroke crisp. */}
|
| 676 |
+
<svg
|
| 677 |
+
viewBox="0 0 100 100"
|
| 678 |
+
preserveAspectRatio="none"
|
| 679 |
+
style={{
|
| 680 |
+
position: "absolute",
|
| 681 |
+
inset: 0,
|
| 682 |
+
width: "100%",
|
| 683 |
+
height: "100%",
|
| 684 |
+
pointerEvents: "none",
|
| 685 |
+
}}
|
| 686 |
+
aria-hidden
|
| 687 |
+
>
|
| 688 |
+
<path
|
| 689 |
+
d={d}
|
| 690 |
+
fill="none"
|
| 691 |
+
stroke="var(--accent)"
|
| 692 |
+
strokeWidth={1.5}
|
| 693 |
+
vectorEffect="non-scaling-stroke"
|
| 694 |
+
strokeLinejoin="round"
|
| 695 |
+
strokeLinecap="round"
|
| 696 |
+
/>
|
| 697 |
+
</svg>
|
| 698 |
+
|
| 699 |
+
{/* Background sample dots — every dated model that's NOT on
|
| 700 |
+
the frontier. Rendered as HTML so they're crisp circles
|
| 701 |
+
and individually clickable / focusable. */}
|
| 702 |
+
{bgSamples.map((s, i) => (
|
| 703 |
+
<button
|
| 704 |
+
key={`s-${i}`}
|
| 705 |
+
type="button"
|
| 706 |
+
aria-label={`${s.name || "Model"} · ${formatMonthYear(s.time)} · ${formatValue(s.score, unit)}`}
|
| 707 |
+
onMouseEnter={(event) => {
|
| 708 |
+
const rect = event.currentTarget.parentElement!.getBoundingClientRect()
|
| 709 |
+
const dot = event.currentTarget.getBoundingClientRect()
|
| 710 |
+
setHover({
|
| 711 |
+
x: dot.left + dot.width / 2 - rect.left,
|
| 712 |
+
y: dot.top + dot.height / 2 - rect.top,
|
| 713 |
+
name: s.name || "Model",
|
| 714 |
+
when: formatMonthYear(s.time),
|
| 715 |
+
score: formatValue(s.score, unit),
|
| 716 |
+
onFrontier: false,
|
| 717 |
+
})
|
| 718 |
+
}}
|
| 719 |
+
onFocus={(event) => {
|
| 720 |
+
const rect = event.currentTarget.parentElement!.getBoundingClientRect()
|
| 721 |
+
const dot = event.currentTarget.getBoundingClientRect()
|
| 722 |
+
setHover({
|
| 723 |
+
x: dot.left + dot.width / 2 - rect.left,
|
| 724 |
+
y: dot.top + dot.height / 2 - rect.top,
|
| 725 |
+
name: s.name || "Model",
|
| 726 |
+
when: formatMonthYear(s.time),
|
| 727 |
+
score: formatValue(s.score, unit),
|
| 728 |
+
onFrontier: false,
|
| 729 |
+
})
|
| 730 |
+
}}
|
| 731 |
+
style={{
|
| 732 |
+
position: "absolute",
|
| 733 |
+
left: `${xPct(s.time)}%`,
|
| 734 |
+
top: `${yPct(s.score)}%`,
|
| 735 |
+
transform: "translate(-50%, -50%)",
|
| 736 |
+
width: 7,
|
| 737 |
+
height: 7,
|
| 738 |
+
borderRadius: "50%",
|
| 739 |
+
background: "var(--fg-subtle)",
|
| 740 |
+
opacity: 0.4,
|
| 741 |
+
border: "none",
|
| 742 |
+
padding: 0,
|
| 743 |
+
cursor: "pointer",
|
| 744 |
+
}}
|
| 745 |
+
/>
|
| 746 |
+
))}
|
| 747 |
+
|
| 748 |
+
{/* Frontier-crossing dots, foregrounded. */}
|
| 749 |
+
{events.map((e, i) => (
|
| 750 |
+
<button
|
| 751 |
+
key={`e-${i}`}
|
| 752 |
+
type="button"
|
| 753 |
+
aria-label={`Frontier: ${e.name || "Model"} · ${formatMonthYear(e.time)} · ${formatValue(e.score, unit)}`}
|
| 754 |
+
onMouseEnter={(event) => {
|
| 755 |
+
const rect = event.currentTarget.parentElement!.getBoundingClientRect()
|
| 756 |
+
const dot = event.currentTarget.getBoundingClientRect()
|
| 757 |
+
setHover({
|
| 758 |
+
x: dot.left + dot.width / 2 - rect.left,
|
| 759 |
+
y: dot.top + dot.height / 2 - rect.top,
|
| 760 |
+
name: e.name || "Model",
|
| 761 |
+
when: formatMonthYear(e.time),
|
| 762 |
+
score: formatValue(e.score, unit),
|
| 763 |
+
onFrontier: true,
|
| 764 |
+
})
|
| 765 |
+
}}
|
| 766 |
+
onFocus={(event) => {
|
| 767 |
+
const rect = event.currentTarget.parentElement!.getBoundingClientRect()
|
| 768 |
+
const dot = event.currentTarget.getBoundingClientRect()
|
| 769 |
+
setHover({
|
| 770 |
+
x: dot.left + dot.width / 2 - rect.left,
|
| 771 |
+
y: dot.top + dot.height / 2 - rect.top,
|
| 772 |
+
name: e.name || "Model",
|
| 773 |
+
when: formatMonthYear(e.time),
|
| 774 |
+
score: formatValue(e.score, unit),
|
| 775 |
+
onFrontier: true,
|
| 776 |
+
})
|
| 777 |
+
}}
|
| 778 |
+
style={{
|
| 779 |
+
position: "absolute",
|
| 780 |
+
left: `${xPct(e.time)}%`,
|
| 781 |
+
top: `${yPct(e.score)}%`,
|
| 782 |
+
transform: "translate(-50%, -50%)",
|
| 783 |
+
width: 11,
|
| 784 |
+
height: 11,
|
| 785 |
+
borderRadius: "50%",
|
| 786 |
+
background: "var(--accent)",
|
| 787 |
+
border: "1.5px solid var(--bg)",
|
| 788 |
+
padding: 0,
|
| 789 |
+
cursor: "pointer",
|
| 790 |
+
boxShadow: "0 0 0 0.5px var(--accent)",
|
| 791 |
+
}}
|
| 792 |
+
/>
|
| 793 |
+
))}
|
| 794 |
+
|
| 795 |
+
{/* Hover nameplate */}
|
| 796 |
+
{hover && (
|
| 797 |
+
<div
|
| 798 |
+
role="status"
|
| 799 |
+
style={{
|
| 800 |
+
position: "absolute",
|
| 801 |
+
left: hover.x,
|
| 802 |
+
top: hover.y - 14,
|
| 803 |
+
transform: "translate(-50%, -100%)",
|
| 804 |
+
pointerEvents: "none",
|
| 805 |
+
background: "var(--fg)",
|
| 806 |
+
color: "var(--bg)",
|
| 807 |
+
padding: "5px 9px",
|
| 808 |
+
fontSize: 11,
|
| 809 |
+
lineHeight: 1.3,
|
| 810 |
+
whiteSpace: "nowrap",
|
| 811 |
+
fontFamily: "var(--font-sans, inherit)",
|
| 812 |
+
boxShadow: "var(--shadow-card, 0 2px 6px rgba(0,0,0,0.18))",
|
| 813 |
+
zIndex: 2,
|
| 814 |
+
}}
|
| 815 |
+
>
|
| 816 |
+
<div style={{ fontWeight: 600 }}>{hover.name}</div>
|
| 817 |
+
<div
|
| 818 |
+
className="font-mono"
|
| 819 |
+
style={{
|
| 820 |
+
fontSize: 10,
|
| 821 |
+
letterSpacing: "0.04em",
|
| 822 |
+
opacity: 0.8,
|
| 823 |
+
marginTop: 1,
|
| 824 |
+
}}
|
| 825 |
+
>
|
| 826 |
+
{hover.when} · {hover.score}
|
| 827 |
+
{hover.onFrontier ? " · frontier" : ""}
|
| 828 |
+
</div>
|
| 829 |
+
</div>
|
| 830 |
+
)}
|
| 831 |
+
|
| 832 |
+
{/* Baseline */}
|
| 833 |
+
<div
|
| 834 |
+
aria-hidden
|
| 835 |
+
style={{
|
| 836 |
+
position: "absolute",
|
| 837 |
+
left: 0,
|
| 838 |
+
right: 0,
|
| 839 |
+
bottom: 0,
|
| 840 |
+
height: 1,
|
| 841 |
+
background: "var(--border-strong)",
|
| 842 |
+
}}
|
| 843 |
+
/>
|
| 844 |
+
</div>
|
| 845 |
+
|
| 846 |
+
{/* Year ticks under the baseline */}
|
| 847 |
+
{yearTicks.map((y) => {
|
| 848 |
+
const t = Date.UTC(y, 0, 1)
|
| 849 |
+
if (t < tMin || t > tMax) return null
|
| 850 |
+
return (
|
| 851 |
+
<div
|
| 852 |
+
key={y}
|
| 853 |
+
aria-hidden
|
| 854 |
+
style={{
|
| 855 |
+
position: "absolute",
|
| 856 |
+
left: `${xPct(t)}%`,
|
| 857 |
+
bottom: 4,
|
| 858 |
+
transform: "translateX(-50%)",
|
| 859 |
+
fontFamily: "var(--font-mono)",
|
| 860 |
+
fontSize: 10,
|
| 861 |
+
color: "var(--fg-subtle)",
|
| 862 |
+
letterSpacing: "0.06em",
|
| 863 |
+
}}
|
| 864 |
+
>
|
| 865 |
+
{y}
|
| 866 |
+
</div>
|
| 867 |
+
)
|
| 868 |
+
})}
|
| 869 |
+
</div>
|
| 870 |
+
|
| 871 |
+
<div
|
| 872 |
+
className="mt-1 flex flex-wrap items-baseline font-mono"
|
| 873 |
+
style={{
|
| 874 |
+
fontSize: 10.5,
|
| 875 |
+
letterSpacing: "0.04em",
|
| 876 |
+
color: "var(--fg-muted)",
|
| 877 |
+
gap: "4px 14px",
|
| 878 |
+
}}
|
| 879 |
+
>
|
| 880 |
+
<span className="inline-flex items-baseline gap-1">
|
| 881 |
+
<span
|
| 882 |
+
className="uppercase"
|
| 883 |
+
style={{ fontSize: 9.5, letterSpacing: "0.12em", color: "var(--fg-subtle)" }}
|
| 884 |
+
>
|
| 885 |
+
frontier
|
| 886 |
+
</span>
|
| 887 |
+
<span className="tabular-nums" style={{ color: "var(--fg)", fontWeight: 600 }}>
|
| 888 |
+
{events.length} step{events.length === 1 ? "" : "s"}
|
| 889 |
+
</span>
|
| 890 |
+
</span>
|
| 891 |
+
<span className="inline-flex items-baseline gap-1">
|
| 892 |
+
<span style={{ color: "var(--fg-subtle)" }}>·</span>
|
| 893 |
+
<span
|
| 894 |
+
className="uppercase"
|
| 895 |
+
style={{ fontSize: 9.5, letterSpacing: "0.12em", color: "var(--fg-subtle)" }}
|
| 896 |
+
>
|
| 897 |
+
best
|
| 898 |
+
</span>
|
| 899 |
+
<span className="tabular-nums" style={{ color: "var(--fg)" }}>
|
| 900 |
+
{formatValue(events[events.length - 1]?.score, unit)}
|
| 901 |
+
</span>
|
| 902 |
+
<span style={{ color: "var(--fg-subtle)" }}>by</span>
|
| 903 |
+
<span style={{ color: "var(--fg)" }}>{events[events.length - 1]?.name || "—"}</span>
|
| 904 |
+
</span>
|
| 905 |
+
<span className="inline-flex items-baseline gap-1">
|
| 906 |
+
<span style={{ color: "var(--fg-subtle)" }}>·</span>
|
| 907 |
+
<span
|
| 908 |
+
className="uppercase"
|
| 909 |
+
style={{ fontSize: 9.5, letterSpacing: "0.12em", color: "var(--fg-subtle)" }}
|
| 910 |
+
>
|
| 911 |
+
since
|
| 912 |
+
</span>
|
| 913 |
+
<span className="tabular-nums" style={{ color: "var(--fg)" }}>
|
| 914 |
+
{formatMonthYear(events[0].time)}
|
| 915 |
+
</span>
|
| 916 |
+
</span>
|
| 917 |
+
<span className="inline-flex items-baseline gap-1" style={{ color: "var(--fg-subtle)" }}>
|
| 918 |
+
<span>·</span>
|
| 919 |
+
<span style={{ fontSize: 9 }}>
|
| 920 |
+
{lowerIsBetter ? "frontier descends — lower is better" : "frontier ascends — higher is better"}
|
| 921 |
+
</span>
|
| 922 |
+
</span>
|
| 923 |
+
</div>
|
| 924 |
+
</div>
|
| 925 |
+
)
|
| 926 |
+
}
|
|
@@ -533,12 +533,13 @@ function consolidateDedicatedHomeBenchmarks(h: CleanableHierarchy) {
|
|
| 533 |
return sliceCount + metricCount
|
| 534 |
}
|
| 535 |
|
| 536 |
-
//
|
| 537 |
-
|
|
|
|
| 538 |
const visit = (b: HierarchyBenchmark) => {
|
| 539 |
-
const
|
| 540 |
-
|
| 541 |
-
|
| 542 |
}
|
| 543 |
for (const fam of h.families ?? []) {
|
| 544 |
for (const b of fam.benchmarks ?? []) visit(b)
|
|
@@ -548,11 +549,28 @@ function consolidateDedicatedHomeBenchmarks(h: CleanableHierarchy) {
|
|
| 548 |
}
|
| 549 |
}
|
| 550 |
|
| 551 |
-
|
| 552 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 553 |
const isPoorerDuplicate = (b: HierarchyBenchmark): boolean => {
|
| 554 |
-
const
|
| 555 |
-
|
|
|
|
|
|
|
|
|
|
| 556 |
}
|
| 557 |
|
| 558 |
for (const fam of h.families ?? []) {
|
|
@@ -640,7 +658,26 @@ function consolidateDedicatedHomeBenchmarks(h: CleanableHierarchy) {
|
|
| 640 |
return false
|
| 641 |
}
|
| 642 |
|
| 643 |
-
// (a) strict-subset
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 644 |
for (const a of allFamilies) {
|
| 645 |
if (dropped.has(a)) continue
|
| 646 |
if (PROTECTED_LEADERBOARD_FAMILIES.has(a.key)) continue
|
|
@@ -648,6 +685,7 @@ function consolidateDedicatedHomeBenchmarks(h: CleanableHierarchy) {
|
|
| 648 |
if (aBenches.length === 0) continue
|
| 649 |
for (const b of allFamilies) {
|
| 650 |
if (a === b || dropped.has(b)) continue
|
|
|
|
| 651 |
const bBenches = benchesByFam.get(b) ?? []
|
| 652 |
if (bBenches.length <= aBenches.length) continue
|
| 653 |
const bByKey = new Map(bBenches.map((h) => [h.bench.key, h.bench]))
|
|
|
|
| 533 |
return sliceCount + metricCount
|
| 534 |
}
|
| 535 |
|
| 536 |
+
// Collect every appearance of every benchmark, indexed by key, so the
|
| 537 |
+
// duplicate check can compare each instance against its peers.
|
| 538 |
+
const instancesByKey = new Map<string, HierarchyBenchmark[]>()
|
| 539 |
const visit = (b: HierarchyBenchmark) => {
|
| 540 |
+
const list = instancesByKey.get(b.key)
|
| 541 |
+
if (list) list.push(b)
|
| 542 |
+
else instancesByKey.set(b.key, [b])
|
| 543 |
}
|
| 544 |
for (const fam of h.families ?? []) {
|
| 545 |
for (const b of fam.benchmarks ?? []) visit(b)
|
|
|
|
| 549 |
}
|
| 550 |
}
|
| 551 |
|
| 552 |
+
const sharesAnyEvalId = (a: HierarchyBenchmark, b: HierarchyBenchmark) => {
|
| 553 |
+
const aIds = new Set(a.summary_eval_ids ?? [])
|
| 554 |
+
if (aIds.size === 0) return false
|
| 555 |
+
for (const id of b.summary_eval_ids ?? []) if (aIds.has(id)) return true
|
| 556 |
+
return false
|
| 557 |
+
}
|
| 558 |
+
|
| 559 |
+
// Drop strictly-poorer copies, BUT only when the poor copy shares an
|
| 560 |
+
// eval_summary_id with a richer instance. Without that gate, a curated
|
| 561 |
+
// family (e.g. `gaia` reporting GAIA at richness 2 with eval_id
|
| 562 |
+
// `gaia%2Fgaia`) gets nuked just because some unrelated source family
|
| 563 |
+
// (`hal` reporting GAIA at richness 8 with eval_id `hal%2Fgaia`)
|
| 564 |
+
// happens to use the same bench key — they're different physical
|
| 565 |
+
// rows and both deserve to surface. With the gate, livebench's
|
| 566 |
+
// structurally-poorer rows still lose to live-bench since they share
|
| 567 |
+
// eval_ids (`live-bench%2Flivebench-coding` lives in both).
|
| 568 |
const isPoorerDuplicate = (b: HierarchyBenchmark): boolean => {
|
| 569 |
+
const peers = instancesByKey.get(b.key) ?? []
|
| 570 |
+
const r = richness(b)
|
| 571 |
+
return peers.some(
|
| 572 |
+
(peer) => peer !== b && richness(peer) > r && sharesAnyEvalId(peer, b),
|
| 573 |
+
)
|
| 574 |
}
|
| 575 |
|
| 576 |
for (const fam of h.families ?? []) {
|
|
|
|
| 658 |
return false
|
| 659 |
}
|
| 660 |
|
| 661 |
+
// (a) strict-subset.
|
| 662 |
+
//
|
| 663 |
+
// Only fires when A's family key is textually related to B's — they
|
| 664 |
+
// slugify the same, or one's slug contains the other. Without that
|
| 665 |
+
// gate the rule was eating curated benchmark families like
|
| 666 |
+
// `tau2-bench` whenever its rows happened to all be republished by a
|
| 667 |
+
// single source family (`exgentic-open-agent`). Those aren't redundant
|
| 668 |
+
// wrappers — they're separate curated leaderboards that the upstream
|
| 669 |
+
// pipeline intentionally surfaces. The intended target is wrappers
|
| 670 |
+
// like `livebench` (slug `livebench`) being a strict subset of
|
| 671 |
+
// `live-bench` (slug `livebench`), where the slugs match exactly.
|
| 672 |
+
const slugForKey = (key: string) =>
|
| 673 |
+
key.toLowerCase().replace(/[^a-z0-9]+/g, "")
|
| 674 |
+
const familyKeysRelated = (aKey: string, bKey: string) => {
|
| 675 |
+
const aSlug = slugForKey(aKey)
|
| 676 |
+
const bSlug = slugForKey(bKey)
|
| 677 |
+
if (!aSlug || !bSlug) return false
|
| 678 |
+
return aSlug === bSlug || aSlug.includes(bSlug) || bSlug.includes(aSlug)
|
| 679 |
+
}
|
| 680 |
+
|
| 681 |
for (const a of allFamilies) {
|
| 682 |
if (dropped.has(a)) continue
|
| 683 |
if (PROTECTED_LEADERBOARD_FAMILIES.has(a.key)) continue
|
|
|
|
| 685 |
if (aBenches.length === 0) continue
|
| 686 |
for (const b of allFamilies) {
|
| 687 |
if (a === b || dropped.has(b)) continue
|
| 688 |
+
if (!familyKeysRelated(a.key, b.key)) continue
|
| 689 |
const bBenches = benchesByFam.get(b) ?? []
|
| 690 |
if (bBenches.length <= aBenches.length) continue
|
| 691 |
const bByKey = new Map(bBenches.map((h) => [h.bench.key, h.bench]))
|