Spaces:
Running
Group model/eval-detail benchmarks by hierarchy.json families
Browse filesThe eval row's `family_id` is null for ~7% of evals (e.g. CySE2
composites) and points at the leaf for ~70% (singleton families),
so the model detail and eval detail pages flattened the curated
family→composite groupings shipped in hierarchy.json. The
`evalHierarchy` prop on `BenchmarkDetail` was destructured but
never read.
Add `lib/hierarchy-lookup.ts` to map each `eval_summary_id` to
its family/composite from hierarchy.json, and consume it from the
four buckets that drive the visible UI: `groupByComposite`,
`listFamiliesByCategory`, and `plotboxUnits` in benchmark-detail,
plus the eval-detail header / sub-benchmark grid in evals/[id].
Falls back to `evaluation.family_id` when the hierarchy has no
entry. The 31 multi-family eval IDs disambiguate via the eval
row's own `family_id` when it's set.
- app/evals/[id]/page.tsx +165 -64
- components/benchmark-detail.tsx +94 -14
- components/eval-detail.tsx +18 -4
- lib/hierarchy-lookup.ts +92 -0
|
@@ -9,9 +9,14 @@ import { EvalDetail } from "@/components/eval-detail"
|
|
| 9 |
import { ParamRangePicker } from "@/components/param-range-picker"
|
| 10 |
import { useAudienceMode } from "@/components/audience-mode-provider"
|
| 11 |
import type { BenchmarkEvalSummary } from "@/lib/eval-processing"
|
| 12 |
-
import { fetchEvalSummary } from "@/lib/dashboard-data-client"
|
| 13 |
import { humanizeEvaluationId } from "@/lib/utils"
|
| 14 |
import { PARAM_RANGE_MAX_INDEX, parseParamsBillionsFromModelName, paramStepToNumeric } from "@/lib/param-range"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
export default function EvalDetailPage() {
|
| 17 |
const params = useParams()
|
|
@@ -20,6 +25,7 @@ export default function EvalDetailPage() {
|
|
| 20 |
const searchParams = useSearchParams()
|
| 21 |
const [summary, setSummary] = useState<BenchmarkEvalSummary | null>(null)
|
| 22 |
const [subSummaries, setSubSummaries] = useState<BenchmarkEvalSummary[]>([])
|
|
|
|
| 23 |
const [loading, setLoading] = useState(true)
|
| 24 |
const [error, setError] = useState<string | null>(null)
|
| 25 |
const [matrixSearch, setMatrixSearch] = useState("")
|
|
@@ -49,8 +55,15 @@ export default function EvalDetailPage() {
|
|
| 49 |
const load = async () => {
|
| 50 |
try {
|
| 51 |
const evalId = decodeURIComponent(params.id as string)
|
| 52 |
-
const found = await
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
setSummary(found)
|
|
|
|
| 54 |
document.title = `${found.evaluation_name} | Benchmark`
|
| 55 |
|
| 56 |
if (found.is_aggregated && found.aggregate_sources?.length) {
|
|
@@ -75,6 +88,28 @@ export default function EvalDetailPage() {
|
|
| 75 |
load()
|
| 76 |
}, [params.id])
|
| 77 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
if (loading) {
|
| 79 |
return (
|
| 80 |
<div className="min-h-screen bg-background">
|
|
@@ -126,9 +161,11 @@ export default function EvalDetailPage() {
|
|
| 126 |
matrixSearch={matrixSearch}
|
| 127 |
onMatrixSearchChange={setMatrixSearch}
|
| 128 |
currentDetailHref={currentDetailHref}
|
|
|
|
|
|
|
| 129 |
/>
|
| 130 |
) : (
|
| 131 |
-
<EvalDetail summary={summary} />
|
| 132 |
)}
|
| 133 |
</main>
|
| 134 |
</div>
|
|
@@ -150,12 +187,16 @@ function CompositeEvalView({
|
|
| 150 |
matrixSearch,
|
| 151 |
onMatrixSearchChange,
|
| 152 |
currentDetailHref,
|
|
|
|
|
|
|
| 153 |
}: {
|
| 154 |
summary: BenchmarkEvalSummary
|
| 155 |
subSummaries: BenchmarkEvalSummary[]
|
| 156 |
matrixSearch: string
|
| 157 |
onMatrixSearchChange: (v: string) => void
|
| 158 |
currentDetailHref: string
|
|
|
|
|
|
|
| 159 |
}) {
|
| 160 |
const { mode } = useAudienceMode()
|
| 161 |
const isPolicy = mode === "policy"
|
|
@@ -169,6 +210,12 @@ function CompositeEvalView({
|
|
| 169 |
const limitations = card?.purpose_and_intended_users?.limitations?.trim()
|
| 170 |
const audience = card?.purpose_and_intended_users?.audience
|
| 171 |
const audienceText = Array.isArray(audience) ? audience.join("; ") : audience
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
const lede = isPolicy
|
| 173 |
? overview || goal || `Composite aggregating ${subBenchmarkCount} component benchmarks across ${summary.models_count.toLocaleString()} models.`
|
| 174 |
: goal || overview || `Composite aggregating ${subBenchmarkCount} component benchmarks across ${summary.models_count.toLocaleString()} models.`
|
|
@@ -182,9 +229,9 @@ function CompositeEvalView({
|
|
| 182 |
className="mb-5 flex flex-wrap items-center gap-3 font-mono text-[11px] uppercase tracking-[0.12em]"
|
| 183 |
style={{ color: "var(--fg-muted)" }}
|
| 184 |
>
|
| 185 |
-
{
|
| 186 |
<>
|
| 187 |
-
<span>{
|
| 188 |
<span style={{ color: "var(--fg-subtle)" }}>·</span>
|
| 189 |
</>
|
| 190 |
)}
|
|
@@ -291,6 +338,7 @@ function CompositeEvalView({
|
|
| 291 |
sources={sources}
|
| 292 |
subSummaries={subSummaries}
|
| 293 |
currentDetailHref={currentDetailHref}
|
|
|
|
| 294 |
/>
|
| 295 |
) : (
|
| 296 |
<MatrixLeaderboard
|
|
@@ -313,16 +361,47 @@ function SubBenchmarkGrid({
|
|
| 313 |
sources,
|
| 314 |
subSummaries,
|
| 315 |
currentDetailHref,
|
|
|
|
| 316 |
}: {
|
| 317 |
sources: NonNullable<BenchmarkEvalSummary["aggregate_sources"]>
|
| 318 |
subSummaries: BenchmarkEvalSummary[]
|
| 319 |
currentDetailHref: string
|
|
|
|
| 320 |
}) {
|
| 321 |
const subMap = useMemo(
|
| 322 |
() => new Map(subSummaries.map((s) => [s.evaluation_id, s])),
|
| 323 |
[subSummaries]
|
| 324 |
)
|
| 325 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
if (sources.length === 0) {
|
| 327 |
return (
|
| 328 |
<div className="ec-card" style={{ padding: 32, textAlign: "center" }}>
|
|
@@ -331,67 +410,89 @@ function SubBenchmarkGrid({
|
|
| 331 |
)
|
| 332 |
}
|
| 333 |
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 349 |
>
|
| 350 |
-
<
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
{source.models_count} model{source.models_count === 1 ? "" : "s"}
|
| 354 |
-
</div>
|
| 355 |
-
</div>
|
| 356 |
-
<h3 className="fam-card-name group-hover:text-[color:var(--accent)] transition-colors">
|
| 357 |
-
{card?.benchmark_details?.name ?? source.composite_benchmark_name}
|
| 358 |
-
</h3>
|
| 359 |
-
<div className="fam-card-org">{humanizeEvaluationId(source.evaluation_id)}</div>
|
| 360 |
-
{summaryLine && (
|
| 361 |
-
<p className="fam-card-summary line-clamp-3">{summaryLine}</p>
|
| 362 |
-
)}
|
| 363 |
-
{sub?.best_model && (
|
| 364 |
-
<div
|
| 365 |
-
className="mt-3 pt-3 text-[12px]"
|
| 366 |
-
style={{
|
| 367 |
-
borderTop: "1px dashed var(--border-soft)",
|
| 368 |
-
color: "var(--fg-muted)",
|
| 369 |
-
}}
|
| 370 |
-
>
|
| 371 |
-
<span
|
| 372 |
-
className="font-mono uppercase tracking-[0.12em] mr-2"
|
| 373 |
-
style={{ fontSize: 9.5, color: "var(--fg-subtle)" }}
|
| 374 |
-
>
|
| 375 |
-
Top
|
| 376 |
-
</span>
|
| 377 |
-
<span style={{ color: "var(--fg)", fontWeight: 600 }}>
|
| 378 |
-
{sub.best_model.name}
|
| 379 |
-
</span>
|
| 380 |
-
<span className="ml-1 font-mono tabular-nums" style={{ color: "var(--fg-muted)" }}>
|
| 381 |
-
{(sub.best_model.score * 100).toFixed(1)}%
|
| 382 |
-
</span>
|
| 383 |
-
</div>
|
| 384 |
-
)}
|
| 385 |
-
<div
|
| 386 |
-
className="mt-3 inline-flex items-center gap-1 font-mono text-[10px] uppercase tracking-[0.12em]"
|
| 387 |
-
style={{ color: "var(--accent)" }}
|
| 388 |
>
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
<
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
</div>
|
| 396 |
)
|
| 397 |
}
|
|
|
|
| 9 |
import { ParamRangePicker } from "@/components/param-range-picker"
|
| 10 |
import { useAudienceMode } from "@/components/audience-mode-provider"
|
| 11 |
import type { BenchmarkEvalSummary } from "@/lib/eval-processing"
|
| 12 |
+
import { fetchEvalHierarchy, fetchEvalSummary } from "@/lib/dashboard-data-client"
|
| 13 |
import { humanizeEvaluationId } from "@/lib/utils"
|
| 14 |
import { PARAM_RANGE_MAX_INDEX, parseParamsBillionsFromModelName, paramStepToNumeric } from "@/lib/param-range"
|
| 15 |
+
import type { EvalHierarchy } from "@/lib/backend-artifacts"
|
| 16 |
+
import {
|
| 17 |
+
buildHierarchyEvalIndex,
|
| 18 |
+
type HierarchyEvalLocation,
|
| 19 |
+
} from "@/lib/hierarchy-lookup"
|
| 20 |
|
| 21 |
export default function EvalDetailPage() {
|
| 22 |
const params = useParams()
|
|
|
|
| 25 |
const searchParams = useSearchParams()
|
| 26 |
const [summary, setSummary] = useState<BenchmarkEvalSummary | null>(null)
|
| 27 |
const [subSummaries, setSubSummaries] = useState<BenchmarkEvalSummary[]>([])
|
| 28 |
+
const [hierarchy, setHierarchy] = useState<EvalHierarchy | null>(null)
|
| 29 |
const [loading, setLoading] = useState(true)
|
| 30 |
const [error, setError] = useState<string | null>(null)
|
| 31 |
const [matrixSearch, setMatrixSearch] = useState("")
|
|
|
|
| 55 |
const load = async () => {
|
| 56 |
try {
|
| 57 |
const evalId = decodeURIComponent(params.id as string)
|
| 58 |
+
const [found, evalHierarchy] = await Promise.all([
|
| 59 |
+
fetchEvalSummary(evalId),
|
| 60 |
+
fetchEvalHierarchy().catch((err) => {
|
| 61 |
+
console.warn("Failed to load eval-hierarchy:", err)
|
| 62 |
+
return null as EvalHierarchy | null
|
| 63 |
+
}),
|
| 64 |
+
])
|
| 65 |
setSummary(found)
|
| 66 |
+
setHierarchy(evalHierarchy)
|
| 67 |
document.title = `${found.evaluation_name} | Benchmark`
|
| 68 |
|
| 69 |
if (found.is_aggregated && found.aggregate_sources?.length) {
|
|
|
|
| 88 |
load()
|
| 89 |
}, [params.id])
|
| 90 |
|
| 91 |
+
const hierarchyIndex = useMemo(() => {
|
| 92 |
+
if (!hierarchy) return null
|
| 93 |
+
const familyIdHints = new Map<string, string>()
|
| 94 |
+
if (summary?.evaluation_id && summary.family_id) {
|
| 95 |
+
familyIdHints.set(summary.evaluation_id, summary.family_id)
|
| 96 |
+
}
|
| 97 |
+
for (const sub of subSummaries) {
|
| 98 |
+
if (sub.evaluation_id && sub.family_id) {
|
| 99 |
+
familyIdHints.set(sub.evaluation_id, sub.family_id)
|
| 100 |
+
}
|
| 101 |
+
}
|
| 102 |
+
return buildHierarchyEvalIndex(
|
| 103 |
+
hierarchy,
|
| 104 |
+
(evalSummaryId) => familyIdHints.get(evalSummaryId) ?? null,
|
| 105 |
+
)
|
| 106 |
+
}, [hierarchy, summary, subSummaries])
|
| 107 |
+
|
| 108 |
+
const hierarchyLocation = useMemo<HierarchyEvalLocation | null>(() => {
|
| 109 |
+
if (!hierarchyIndex || !summary?.evaluation_id) return null
|
| 110 |
+
return hierarchyIndex.get(summary.evaluation_id) ?? null
|
| 111 |
+
}, [hierarchyIndex, summary])
|
| 112 |
+
|
| 113 |
if (loading) {
|
| 114 |
return (
|
| 115 |
<div className="min-h-screen bg-background">
|
|
|
|
| 161 |
matrixSearch={matrixSearch}
|
| 162 |
onMatrixSearchChange={setMatrixSearch}
|
| 163 |
currentDetailHref={currentDetailHref}
|
| 164 |
+
hierarchyIndex={hierarchyIndex}
|
| 165 |
+
hierarchyLocation={hierarchyLocation}
|
| 166 |
/>
|
| 167 |
) : (
|
| 168 |
+
<EvalDetail summary={summary} hierarchyLocation={hierarchyLocation} />
|
| 169 |
)}
|
| 170 |
</main>
|
| 171 |
</div>
|
|
|
|
| 187 |
matrixSearch,
|
| 188 |
onMatrixSearchChange,
|
| 189 |
currentDetailHref,
|
| 190 |
+
hierarchyIndex,
|
| 191 |
+
hierarchyLocation,
|
| 192 |
}: {
|
| 193 |
summary: BenchmarkEvalSummary
|
| 194 |
subSummaries: BenchmarkEvalSummary[]
|
| 195 |
matrixSearch: string
|
| 196 |
onMatrixSearchChange: (v: string) => void
|
| 197 |
currentDetailHref: string
|
| 198 |
+
hierarchyIndex: Map<string, HierarchyEvalLocation> | null
|
| 199 |
+
hierarchyLocation: HierarchyEvalLocation | null
|
| 200 |
}) {
|
| 201 |
const { mode } = useAudienceMode()
|
| 202 |
const isPolicy = mode === "policy"
|
|
|
|
| 210 |
const limitations = card?.purpose_and_intended_users?.limitations?.trim()
|
| 211 |
const audience = card?.purpose_and_intended_users?.audience
|
| 212 |
const audienceText = Array.isArray(audience) ? audience.join("; ") : audience
|
| 213 |
+
const familyHeader =
|
| 214 |
+
hierarchyLocation?.familyDisplayName && hierarchyLocation.familyDisplayName !== summary.evaluation_name
|
| 215 |
+
? hierarchyLocation.familyDisplayName
|
| 216 |
+
: summary.composite_benchmark_name && summary.composite_benchmark_name !== summary.evaluation_name
|
| 217 |
+
? summary.composite_benchmark_name
|
| 218 |
+
: null
|
| 219 |
const lede = isPolicy
|
| 220 |
? overview || goal || `Composite aggregating ${subBenchmarkCount} component benchmarks across ${summary.models_count.toLocaleString()} models.`
|
| 221 |
: goal || overview || `Composite aggregating ${subBenchmarkCount} component benchmarks across ${summary.models_count.toLocaleString()} models.`
|
|
|
|
| 229 |
className="mb-5 flex flex-wrap items-center gap-3 font-mono text-[11px] uppercase tracking-[0.12em]"
|
| 230 |
style={{ color: "var(--fg-muted)" }}
|
| 231 |
>
|
| 232 |
+
{familyHeader && (
|
| 233 |
<>
|
| 234 |
+
<span>{familyHeader}</span>
|
| 235 |
<span style={{ color: "var(--fg-subtle)" }}>·</span>
|
| 236 |
</>
|
| 237 |
)}
|
|
|
|
| 338 |
sources={sources}
|
| 339 |
subSummaries={subSummaries}
|
| 340 |
currentDetailHref={currentDetailHref}
|
| 341 |
+
hierarchyIndex={hierarchyIndex}
|
| 342 |
/>
|
| 343 |
) : (
|
| 344 |
<MatrixLeaderboard
|
|
|
|
| 361 |
sources,
|
| 362 |
subSummaries,
|
| 363 |
currentDetailHref,
|
| 364 |
+
hierarchyIndex,
|
| 365 |
}: {
|
| 366 |
sources: NonNullable<BenchmarkEvalSummary["aggregate_sources"]>
|
| 367 |
subSummaries: BenchmarkEvalSummary[]
|
| 368 |
currentDetailHref: string
|
| 369 |
+
hierarchyIndex: Map<string, HierarchyEvalLocation> | null
|
| 370 |
}) {
|
| 371 |
const subMap = useMemo(
|
| 372 |
() => new Map(subSummaries.map((s) => [s.evaluation_id, s])),
|
| 373 |
[subSummaries]
|
| 374 |
)
|
| 375 |
|
| 376 |
+
// Group sources by hierarchy family. When the hierarchy doesn't resolve a
|
| 377 |
+
// family for a source (or no hierarchy was loaded), bucket those entries
|
| 378 |
+
// under a single "Other" section instead of spamming per-eval headers.
|
| 379 |
+
type FamilyBucket = {
|
| 380 |
+
key: string
|
| 381 |
+
displayName: string | null
|
| 382 |
+
sources: NonNullable<BenchmarkEvalSummary["aggregate_sources"]>
|
| 383 |
+
}
|
| 384 |
+
const buckets = useMemo<FamilyBucket[]>(() => {
|
| 385 |
+
if (!hierarchyIndex) {
|
| 386 |
+
return [{ key: "__all__", displayName: null, sources }]
|
| 387 |
+
}
|
| 388 |
+
const ordered: FamilyBucket[] = []
|
| 389 |
+
const byKey = new Map<string, FamilyBucket>()
|
| 390 |
+
for (const source of sources) {
|
| 391 |
+
const location = hierarchyIndex.get(source.evaluation_id) ?? null
|
| 392 |
+
const key = location?.familyKey ?? "__unmapped__"
|
| 393 |
+
const displayName = location?.familyDisplayName ?? null
|
| 394 |
+
let bucket = byKey.get(key)
|
| 395 |
+
if (!bucket) {
|
| 396 |
+
bucket = { key, displayName, sources: [] }
|
| 397 |
+
byKey.set(key, bucket)
|
| 398 |
+
ordered.push(bucket)
|
| 399 |
+
}
|
| 400 |
+
bucket.sources.push(source)
|
| 401 |
+
}
|
| 402 |
+
return ordered
|
| 403 |
+
}, [sources, hierarchyIndex])
|
| 404 |
+
|
| 405 |
if (sources.length === 0) {
|
| 406 |
return (
|
| 407 |
<div className="ec-card" style={{ padding: 32, textAlign: "center" }}>
|
|
|
|
| 410 |
)
|
| 411 |
}
|
| 412 |
|
| 413 |
+
const renderCard = (source: NonNullable<BenchmarkEvalSummary["aggregate_sources"]>[number]) => {
|
| 414 |
+
const sub = subMap.get(source.evaluation_id)
|
| 415 |
+
const card = sub?.benchmark_card
|
| 416 |
+
const overview = card?.benchmark_details?.overview ?? sub?.metric_config?.evaluation_description
|
| 417 |
+
const goal = card?.purpose_and_intended_users?.goal
|
| 418 |
+
const summaryLine = goal || overview
|
| 419 |
+
|
| 420 |
+
return (
|
| 421 |
+
<Link
|
| 422 |
+
key={source.evaluation_id}
|
| 423 |
+
href={`/evals/${source.evaluation_id}?from=${encodeURIComponent(currentDetailHref)}`}
|
| 424 |
+
className="fam-card group block"
|
| 425 |
+
style={{ textDecoration: "none", color: "inherit" }}
|
| 426 |
+
>
|
| 427 |
+
<div className="flex items-start justify-between gap-2 mb-1">
|
| 428 |
+
<div className="fam-card-kind">Component benchmark</div>
|
| 429 |
+
<div className="fam-card-counts">
|
| 430 |
+
{source.models_count} model{source.models_count === 1 ? "" : "s"}
|
| 431 |
+
</div>
|
| 432 |
+
</div>
|
| 433 |
+
<h3 className="fam-card-name group-hover:text-[color:var(--accent)] transition-colors">
|
| 434 |
+
{card?.benchmark_details?.name ?? source.composite_benchmark_name}
|
| 435 |
+
</h3>
|
| 436 |
+
<div className="fam-card-org">{humanizeEvaluationId(source.evaluation_id)}</div>
|
| 437 |
+
{summaryLine && (
|
| 438 |
+
<p className="fam-card-summary line-clamp-3">{summaryLine}</p>
|
| 439 |
+
)}
|
| 440 |
+
{sub?.best_model && (
|
| 441 |
+
<div
|
| 442 |
+
className="mt-3 pt-3 text-[12px]"
|
| 443 |
+
style={{
|
| 444 |
+
borderTop: "1px dashed var(--border-soft)",
|
| 445 |
+
color: "var(--fg-muted)",
|
| 446 |
+
}}
|
| 447 |
>
|
| 448 |
+
<span
|
| 449 |
+
className="font-mono uppercase tracking-[0.12em] mr-2"
|
| 450 |
+
style={{ fontSize: 9.5, color: "var(--fg-subtle)" }}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 451 |
>
|
| 452 |
+
Top
|
| 453 |
+
</span>
|
| 454 |
+
<span style={{ color: "var(--fg)", fontWeight: 600 }}>
|
| 455 |
+
{sub.best_model.name}
|
| 456 |
+
</span>
|
| 457 |
+
<span className="ml-1 font-mono tabular-nums" style={{ color: "var(--fg-muted)" }}>
|
| 458 |
+
{(sub.best_model.score * 100).toFixed(1)}%
|
| 459 |
+
</span>
|
| 460 |
+
</div>
|
| 461 |
+
)}
|
| 462 |
+
<div
|
| 463 |
+
className="mt-3 inline-flex items-center gap-1 font-mono text-[10px] uppercase tracking-[0.12em]"
|
| 464 |
+
style={{ color: "var(--accent)" }}
|
| 465 |
+
>
|
| 466 |
+
Open
|
| 467 |
+
<ArrowUpRight className="h-3 w-3" />
|
| 468 |
+
</div>
|
| 469 |
+
</Link>
|
| 470 |
+
)
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
// Render flat when only a single bucket — the per-family headers add no
|
| 474 |
+
// signal in that case (which is the typical "all components belong to one
|
| 475 |
+
// family" composite).
|
| 476 |
+
if (buckets.length <= 1) {
|
| 477 |
+
return <div className="fam-grid">{sources.map(renderCard)}</div>
|
| 478 |
+
}
|
| 479 |
+
|
| 480 |
+
return (
|
| 481 |
+
<div className="space-y-8">
|
| 482 |
+
{buckets.map((bucket) => (
|
| 483 |
+
<section key={bucket.key}>
|
| 484 |
+
<div
|
| 485 |
+
className="kicker mb-3"
|
| 486 |
+
style={{ display: "flex", alignItems: "baseline", gap: 8 }}
|
| 487 |
+
>
|
| 488 |
+
<span>{bucket.displayName ?? "Other"}</span>
|
| 489 |
+
<span style={{ color: "var(--fg-subtle)", fontWeight: 400 }}>
|
| 490 |
+
· {bucket.sources.length} component{bucket.sources.length === 1 ? "" : "s"}
|
| 491 |
+
</span>
|
| 492 |
+
</div>
|
| 493 |
+
<div className="fam-grid">{bucket.sources.map(renderCard)}</div>
|
| 494 |
+
</section>
|
| 495 |
+
))}
|
| 496 |
</div>
|
| 497 |
)
|
| 498 |
}
|
|
@@ -49,6 +49,10 @@ import type {
|
|
| 49 |
EvalHierarchy,
|
| 50 |
SubmissionAxis,
|
| 51 |
} from "@/lib/backend-artifacts"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
import { type CSSProperties, Fragment, useState, useEffect, useMemo } from "react"
|
| 53 |
|
| 54 |
interface BenchmarkDetailProps {
|
|
@@ -268,7 +272,38 @@ function doesLabelMatchSuiteKey(label: string | null | undefined, compositeKey:
|
|
| 268 |
return normalizeCompositeKey(normalizeDisplayKey(label)) === normalizeCompositeKey(compositeKey)
|
| 269 |
}
|
| 270 |
|
| 271 |
-
function
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
const evaluation = group.variants[0]?.evaluation
|
| 273 |
const backendSuiteKey = evaluation?.family_id
|
| 274 |
|
|
@@ -283,7 +318,16 @@ function getCompositeDisplayName(key: string): string {
|
|
| 283 |
return normalizeDisplayLabel(key)
|
| 284 |
}
|
| 285 |
|
| 286 |
-
function getCompositeName(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 287 |
const evaluation = group.variants[0]?.evaluation
|
| 288 |
const benchmarkCardName = group.benchmarkCard?.benchmark_details?.name
|
| 289 |
const backendParentName = evaluation?.benchmark_parent_name
|
|
@@ -307,11 +351,12 @@ function getCompositeName(group: BenchmarkGroup, compositeKey: string): string {
|
|
| 307 |
function groupByComposite(
|
| 308 |
groups: BenchmarkGroup[],
|
| 309 |
modelIds: string[],
|
| 310 |
-
peerRanks: PeerRanksMap
|
|
|
|
| 311 |
): CompositeGroup[] {
|
| 312 |
const composites = new Map<string, BenchmarkGroup[]>()
|
| 313 |
for (const group of groups) {
|
| 314 |
-
const key = getCompositeKey(group)
|
| 315 |
const existing = composites.get(key) ?? []
|
| 316 |
existing.push(group)
|
| 317 |
composites.set(key, existing)
|
|
@@ -335,7 +380,7 @@ function groupByComposite(
|
|
| 335 |
|
| 336 |
return {
|
| 337 |
compositeKey,
|
| 338 |
-
compositeName: benchmarks[0] ? getCompositeName(benchmarks[0], compositeKey) : getCompositeDisplayName(compositeKey),
|
| 339 |
benchmarks,
|
| 340 |
avgRawScore,
|
| 341 |
avgNormalizedScore: avgScore,
|
|
@@ -1663,6 +1708,28 @@ export function BenchmarkDetail({
|
|
| 1663 |
loadPeerRanks().then(setPeerRanks)
|
| 1664 |
}, [])
|
| 1665 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1666 |
// Composite relevance score for benchmark ordering
|
| 1667 |
// relevance = population × 0.4 + rank_extremity × 0.3 + has_metadata × 0.2 + recency × 0.1
|
| 1668 |
const getRelevanceScore = useMemo(() => {
|
|
@@ -1985,8 +2052,13 @@ export function BenchmarkDetail({
|
|
| 1985 |
?.evaluation.eval_summary_id
|
| 1986 |
const evalEntry =
|
| 1987 |
evalId && comparisonIndex ? comparisonIndex.evals[evalId] : null
|
| 1988 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1989 |
const famName =
|
|
|
|
| 1990 |
evalEntry?.family_display_name ||
|
| 1991 |
evalEntry?.display_name ||
|
| 1992 |
group.title
|
|
@@ -2014,31 +2086,31 @@ export function BenchmarkDetail({
|
|
| 2014 |
kind: f.groups.length > 1 ? "multi-eval" as const : "single-eval" as const,
|
| 2015 |
})),
|
| 2016 |
}))
|
| 2017 |
-
}, [filteredBenchmarkGroups, comparisonIndex, summary.categories_covered])
|
| 2018 |
|
| 2019 |
const compositeGroups = useMemo(() => {
|
| 2020 |
-
const groups = groupByComposite(filteredBenchmarkGroups, modelIds, peerRanks)
|
| 2021 |
// Re-sort composites by max relevance of their benchmarks
|
| 2022 |
return groups.sort((a, b) => {
|
| 2023 |
const aMax = Math.max(...a.benchmarks.map(getRelevanceScore))
|
| 2024 |
const bMax = Math.max(...b.benchmarks.map(getRelevanceScore))
|
| 2025 |
return bMax - aMax
|
| 2026 |
})
|
| 2027 |
-
}, [filteredBenchmarkGroups, modelIds, peerRanks, getRelevanceScore])
|
| 2028 |
|
| 2029 |
const categoryCompositeSections = useMemo(
|
| 2030 |
() =>
|
| 2031 |
groupedFilteredBenchmarkGroups
|
| 2032 |
.map(({ category, groups }) => ({
|
| 2033 |
category,
|
| 2034 |
-
composites: groupByComposite(groups, modelIds, peerRanks).sort((a, b) => {
|
| 2035 |
const aMax = Math.max(...a.benchmarks.map(getRelevanceScore))
|
| 2036 |
const bMax = Math.max(...b.benchmarks.map(getRelevanceScore))
|
| 2037 |
return bMax - aMax
|
| 2038 |
}),
|
| 2039 |
}))
|
| 2040 |
.filter((section) => section.composites.length > 0),
|
| 2041 |
-
[groupedFilteredBenchmarkGroups, modelIds, peerRanks, getRelevanceScore]
|
| 2042 |
)
|
| 2043 |
|
| 2044 |
const categoryScoreRanges = useMemo(() => {
|
|
@@ -2456,9 +2528,17 @@ export function BenchmarkDetail({
|
|
| 2456 |
const evalEntry = comparisonIndex.evals[evalId]
|
| 2457 |
if (!evalEntry) continue
|
| 2458 |
|
| 2459 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2460 |
const famName =
|
| 2461 |
-
|
|
|
|
|
|
|
|
|
|
| 2462 |
const bucket = familyBuckets.get(famKey) ?? {
|
| 2463 |
familyName: famName,
|
| 2464 |
category: group.category,
|
|
@@ -2673,7 +2753,7 @@ export function BenchmarkDetail({
|
|
| 2673 |
}
|
| 2674 |
|
| 2675 |
return units
|
| 2676 |
-
}, [comparisonIndex, filteredBenchmarkGroups])
|
| 2677 |
|
| 2678 |
const [activeViewByUnit, setActiveViewByUnit] = useState<Record<string, string>>({})
|
| 2679 |
const [activeMetricByUnit, setActiveMetricByUnit] = useState<Record<string, string>>({})
|
|
|
|
| 49 |
EvalHierarchy,
|
| 50 |
SubmissionAxis,
|
| 51 |
} from "@/lib/backend-artifacts"
|
| 52 |
+
import {
|
| 53 |
+
buildHierarchyEvalIndex,
|
| 54 |
+
type HierarchyEvalLocation,
|
| 55 |
+
} from "@/lib/hierarchy-lookup"
|
| 56 |
import { type CSSProperties, Fragment, useState, useEffect, useMemo } from "react"
|
| 57 |
|
| 58 |
interface BenchmarkDetailProps {
|
|
|
|
| 272 |
return normalizeCompositeKey(normalizeDisplayKey(label)) === normalizeCompositeKey(compositeKey)
|
| 273 |
}
|
| 274 |
|
| 275 |
+
function getHierarchyLocation(
|
| 276 |
+
group: BenchmarkGroup,
|
| 277 |
+
hierarchyIndex: Map<string, HierarchyEvalLocation> | null,
|
| 278 |
+
): HierarchyEvalLocation | undefined {
|
| 279 |
+
if (!hierarchyIndex) {
|
| 280 |
+
return undefined
|
| 281 |
+
}
|
| 282 |
+
for (const variant of group.variants) {
|
| 283 |
+
const evalSummaryId = variant.evaluation.eval_summary_id
|
| 284 |
+
if (evalSummaryId) {
|
| 285 |
+
const location = hierarchyIndex.get(evalSummaryId)
|
| 286 |
+
if (location) {
|
| 287 |
+
return location
|
| 288 |
+
}
|
| 289 |
+
}
|
| 290 |
+
}
|
| 291 |
+
return undefined
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
function getCompositeKey(
|
| 295 |
+
group: BenchmarkGroup,
|
| 296 |
+
hierarchyIndex: Map<string, HierarchyEvalLocation> | null,
|
| 297 |
+
): string {
|
| 298 |
+
// Prefer the curated grouping from hierarchy.json. The eval row's own
|
| 299 |
+
// family_id is null for ~7% of evals (e.g. CySE2 composites) and points
|
| 300 |
+
// at the leaf for singleton families, so the hierarchy is the only
|
| 301 |
+
// source that captures family→composite groupings authoritatively.
|
| 302 |
+
const location = getHierarchyLocation(group, hierarchyIndex)
|
| 303 |
+
if (location) {
|
| 304 |
+
return normalizeCompositeKey(location.familyKey)
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
const evaluation = group.variants[0]?.evaluation
|
| 308 |
const backendSuiteKey = evaluation?.family_id
|
| 309 |
|
|
|
|
| 318 |
return normalizeDisplayLabel(key)
|
| 319 |
}
|
| 320 |
|
| 321 |
+
function getCompositeName(
|
| 322 |
+
group: BenchmarkGroup,
|
| 323 |
+
compositeKey: string,
|
| 324 |
+
hierarchyIndex: Map<string, HierarchyEvalLocation> | null,
|
| 325 |
+
): string {
|
| 326 |
+
const location = getHierarchyLocation(group, hierarchyIndex)
|
| 327 |
+
if (location?.familyDisplayName) {
|
| 328 |
+
return location.familyDisplayName
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
const evaluation = group.variants[0]?.evaluation
|
| 332 |
const benchmarkCardName = group.benchmarkCard?.benchmark_details?.name
|
| 333 |
const backendParentName = evaluation?.benchmark_parent_name
|
|
|
|
| 351 |
function groupByComposite(
|
| 352 |
groups: BenchmarkGroup[],
|
| 353 |
modelIds: string[],
|
| 354 |
+
peerRanks: PeerRanksMap,
|
| 355 |
+
hierarchyIndex: Map<string, HierarchyEvalLocation> | null
|
| 356 |
): CompositeGroup[] {
|
| 357 |
const composites = new Map<string, BenchmarkGroup[]>()
|
| 358 |
for (const group of groups) {
|
| 359 |
+
const key = getCompositeKey(group, hierarchyIndex)
|
| 360 |
const existing = composites.get(key) ?? []
|
| 361 |
existing.push(group)
|
| 362 |
composites.set(key, existing)
|
|
|
|
| 380 |
|
| 381 |
return {
|
| 382 |
compositeKey,
|
| 383 |
+
compositeName: benchmarks[0] ? getCompositeName(benchmarks[0], compositeKey, hierarchyIndex) : getCompositeDisplayName(compositeKey),
|
| 384 |
benchmarks,
|
| 385 |
avgRawScore,
|
| 386 |
avgNormalizedScore: avgScore,
|
|
|
|
| 1708 |
loadPeerRanks().then(setPeerRanks)
|
| 1709 |
}, [])
|
| 1710 |
|
| 1711 |
+
// Build an eval_summary_id → family/composite lookup from hierarchy.json.
|
| 1712 |
+
// ~31 eval_summary_ids appear in multiple families (e.g. mmlu-pro under
|
| 1713 |
+
// both `mmlu` and `artificial-analysis`); use the eval row's own family_id
|
| 1714 |
+
// as the disambiguating preference when present.
|
| 1715 |
+
const hierarchyIndex = useMemo(() => {
|
| 1716 |
+
if (!evalHierarchy) {
|
| 1717 |
+
return null
|
| 1718 |
+
}
|
| 1719 |
+
const familyIdByEvalSummaryId = new Map<string, string>()
|
| 1720 |
+
for (const evals of Object.values(summary.evaluations_by_category)) {
|
| 1721 |
+
for (const evaluation of evals) {
|
| 1722 |
+
if (evaluation.eval_summary_id && evaluation.family_id) {
|
| 1723 |
+
familyIdByEvalSummaryId.set(evaluation.eval_summary_id, evaluation.family_id)
|
| 1724 |
+
}
|
| 1725 |
+
}
|
| 1726 |
+
}
|
| 1727 |
+
return buildHierarchyEvalIndex(
|
| 1728 |
+
evalHierarchy,
|
| 1729 |
+
(evalSummaryId) => familyIdByEvalSummaryId.get(evalSummaryId) ?? null,
|
| 1730 |
+
)
|
| 1731 |
+
}, [evalHierarchy, summary.evaluations_by_category])
|
| 1732 |
+
|
| 1733 |
// Composite relevance score for benchmark ordering
|
| 1734 |
// relevance = population × 0.4 + rank_extremity × 0.3 + has_metadata × 0.2 + recency × 0.1
|
| 1735 |
const getRelevanceScore = useMemo(() => {
|
|
|
|
| 2052 |
?.evaluation.eval_summary_id
|
| 2053 |
const evalEntry =
|
| 2054 |
evalId && comparisonIndex ? comparisonIndex.evals[evalId] : null
|
| 2055 |
+
const hierarchyLocation = evalId
|
| 2056 |
+
? hierarchyIndex?.get(evalId) ?? null
|
| 2057 |
+
: null
|
| 2058 |
+
const famKey =
|
| 2059 |
+
hierarchyLocation?.familyKey ?? evalEntry?.family_id ?? group.key
|
| 2060 |
const famName =
|
| 2061 |
+
hierarchyLocation?.familyDisplayName ||
|
| 2062 |
evalEntry?.family_display_name ||
|
| 2063 |
evalEntry?.display_name ||
|
| 2064 |
group.title
|
|
|
|
| 2086 |
kind: f.groups.length > 1 ? "multi-eval" as const : "single-eval" as const,
|
| 2087 |
})),
|
| 2088 |
}))
|
| 2089 |
+
}, [filteredBenchmarkGroups, comparisonIndex, hierarchyIndex, summary.categories_covered])
|
| 2090 |
|
| 2091 |
const compositeGroups = useMemo(() => {
|
| 2092 |
+
const groups = groupByComposite(filteredBenchmarkGroups, modelIds, peerRanks, hierarchyIndex)
|
| 2093 |
// Re-sort composites by max relevance of their benchmarks
|
| 2094 |
return groups.sort((a, b) => {
|
| 2095 |
const aMax = Math.max(...a.benchmarks.map(getRelevanceScore))
|
| 2096 |
const bMax = Math.max(...b.benchmarks.map(getRelevanceScore))
|
| 2097 |
return bMax - aMax
|
| 2098 |
})
|
| 2099 |
+
}, [filteredBenchmarkGroups, modelIds, peerRanks, getRelevanceScore, hierarchyIndex])
|
| 2100 |
|
| 2101 |
const categoryCompositeSections = useMemo(
|
| 2102 |
() =>
|
| 2103 |
groupedFilteredBenchmarkGroups
|
| 2104 |
.map(({ category, groups }) => ({
|
| 2105 |
category,
|
| 2106 |
+
composites: groupByComposite(groups, modelIds, peerRanks, hierarchyIndex).sort((a, b) => {
|
| 2107 |
const aMax = Math.max(...a.benchmarks.map(getRelevanceScore))
|
| 2108 |
const bMax = Math.max(...b.benchmarks.map(getRelevanceScore))
|
| 2109 |
return bMax - aMax
|
| 2110 |
}),
|
| 2111 |
}))
|
| 2112 |
.filter((section) => section.composites.length > 0),
|
| 2113 |
+
[groupedFilteredBenchmarkGroups, modelIds, peerRanks, getRelevanceScore, hierarchyIndex]
|
| 2114 |
)
|
| 2115 |
|
| 2116 |
const categoryScoreRanges = useMemo(() => {
|
|
|
|
| 2528 |
const evalEntry = comparisonIndex.evals[evalId]
|
| 2529 |
if (!evalEntry) continue
|
| 2530 |
|
| 2531 |
+
// Prefer hierarchy.json grouping. The comparison-index family_id is
|
| 2532 |
+
// null for ~7% of evals (e.g. CySE2 composites) and points at the
|
| 2533 |
+
// leaf for singleton families, so the hierarchy is the only source
|
| 2534 |
+
// that captures family→composite groupings authoritatively.
|
| 2535 |
+
const hierarchyLocation = hierarchyIndex?.get(evalId) ?? null
|
| 2536 |
+
const famKey = hierarchyLocation?.familyKey ?? evalEntry.family_id ?? evalId
|
| 2537 |
const famName =
|
| 2538 |
+
hierarchyLocation?.familyDisplayName ||
|
| 2539 |
+
evalEntry.family_display_name ||
|
| 2540 |
+
evalEntry.display_name ||
|
| 2541 |
+
famKey
|
| 2542 |
const bucket = familyBuckets.get(famKey) ?? {
|
| 2543 |
familyName: famName,
|
| 2544 |
category: group.category,
|
|
|
|
| 2753 |
}
|
| 2754 |
|
| 2755 |
return units
|
| 2756 |
+
}, [comparisonIndex, filteredBenchmarkGroups, hierarchyIndex])
|
| 2757 |
|
| 2758 |
const [activeViewByUnit, setActiveViewByUnit] = useState<Record<string, string>>({})
|
| 2759 |
const [activeMetricByUnit, setActiveMetricByUnit] = useState<Record<string, string>>({})
|
|
@@ -52,6 +52,7 @@ import {
|
|
| 52 |
} from "lucide-react"
|
| 53 |
import type { BenchmarkCard } from "@/lib/benchmark-schema"
|
| 54 |
import type { BenchmarkEvalSummary, ModelResultForBenchmark } from "@/lib/eval-processing"
|
|
|
|
| 55 |
import { PolicyOverview } from "@/components/policy-overview"
|
| 56 |
import { ResearcherReproducibilityCard } from "@/components/researcher-reproducibility-card"
|
| 57 |
import { KnownIssuesPanel } from "@/components/known-issues-panel"
|
|
@@ -61,6 +62,7 @@ import { FlagScoreButton } from "@/components/flag-score-button"
|
|
| 61 |
|
| 62 |
interface EvalDetailProps {
|
| 63 |
summary: BenchmarkEvalSummary
|
|
|
|
| 64 |
}
|
| 65 |
|
| 66 |
interface LeaderboardRow {
|
|
@@ -478,7 +480,7 @@ function getSetupLabel(modelResult: ModelResultForBenchmark): string {
|
|
| 478 |
return parts.join(" ")
|
| 479 |
}
|
| 480 |
|
| 481 |
-
export function EvalDetail({ summary }: EvalDetailProps) {
|
| 482 |
const { mode } = useAudienceMode()
|
| 483 |
const isResearchView = mode === "research"
|
| 484 |
const hasMultiMetricLeaderboard =
|
|
@@ -581,9 +583,21 @@ export function EvalDetail({ summary }: EvalDetailProps) {
|
|
| 581 |
[key]: !current[key],
|
| 582 |
}))
|
| 583 |
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 587 |
|
| 588 |
const heroLede = isResearchView
|
| 589 |
? summary.metric_config.evaluation_description
|
|
|
|
| 52 |
} from "lucide-react"
|
| 53 |
import type { BenchmarkCard } from "@/lib/benchmark-schema"
|
| 54 |
import type { BenchmarkEvalSummary, ModelResultForBenchmark } from "@/lib/eval-processing"
|
| 55 |
+
import type { HierarchyEvalLocation } from "@/lib/hierarchy-lookup"
|
| 56 |
import { PolicyOverview } from "@/components/policy-overview"
|
| 57 |
import { ResearcherReproducibilityCard } from "@/components/researcher-reproducibility-card"
|
| 58 |
import { KnownIssuesPanel } from "@/components/known-issues-panel"
|
|
|
|
| 62 |
|
| 63 |
interface EvalDetailProps {
|
| 64 |
summary: BenchmarkEvalSummary
|
| 65 |
+
hierarchyLocation?: HierarchyEvalLocation | null
|
| 66 |
}
|
| 67 |
|
| 68 |
interface LeaderboardRow {
|
|
|
|
| 480 |
return parts.join(" ")
|
| 481 |
}
|
| 482 |
|
| 483 |
+
export function EvalDetail({ summary, hierarchyLocation }: EvalDetailProps) {
|
| 484 |
const { mode } = useAudienceMode()
|
| 485 |
const isResearchView = mode === "research"
|
| 486 |
const hasMultiMetricLeaderboard =
|
|
|
|
| 583 |
[key]: !current[key],
|
| 584 |
}))
|
| 585 |
|
| 586 |
+
// Prefer the curated family name from hierarchy.json — the producer's
|
| 587 |
+
// composite_benchmark_name often equals the eval's own slug (e.g.
|
| 588 |
+
// "cyse2_interpreter_abuse"), so the header repeats itself. When the
|
| 589 |
+
// hierarchy resolves a different parent family ("CySE2"), use that instead.
|
| 590 |
+
const hierarchyHeaderOrg = (() => {
|
| 591 |
+
const familyName = hierarchyLocation?.familyDisplayName?.trim()
|
| 592 |
+
if (!familyName || familyName === summary.evaluation_name) {
|
| 593 |
+
return null
|
| 594 |
+
}
|
| 595 |
+
return familyName
|
| 596 |
+
})()
|
| 597 |
+
const headerOrg = hierarchyHeaderOrg
|
| 598 |
+
?? (summary.composite_benchmark_name && summary.composite_benchmark_name !== summary.evaluation_name
|
| 599 |
+
? summary.composite_benchmark_name
|
| 600 |
+
: null)
|
| 601 |
|
| 602 |
const heroLede = isResearchView
|
| 603 |
? summary.metric_config.evaluation_description
|
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type {
|
| 2 |
+
EvalHierarchy,
|
| 3 |
+
HierarchyComposite,
|
| 4 |
+
HierarchyFamily,
|
| 5 |
+
} from "@/lib/backend-artifacts"
|
| 6 |
+
|
| 7 |
+
export interface HierarchyEvalLocation {
|
| 8 |
+
familyKey: string
|
| 9 |
+
familyDisplayName: string
|
| 10 |
+
compositeKey?: string
|
| 11 |
+
compositeDisplayName?: string
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
interface FamilyAppearance {
|
| 15 |
+
family: HierarchyFamily
|
| 16 |
+
composite?: HierarchyComposite
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
function findComposite(
|
| 20 |
+
family: HierarchyFamily,
|
| 21 |
+
evalSummaryId: string,
|
| 22 |
+
): HierarchyComposite | undefined {
|
| 23 |
+
const composites = family.composites
|
| 24 |
+
if (!composites?.length) {
|
| 25 |
+
return undefined
|
| 26 |
+
}
|
| 27 |
+
const sourcePrefix = evalSummaryId.split("%2F")[0]
|
| 28 |
+
return composites.find((composite) => composite.key === sourcePrefix)
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
function buildAppearancesIndex(
|
| 32 |
+
hierarchy: EvalHierarchy | null | undefined,
|
| 33 |
+
): Map<string, FamilyAppearance[]> {
|
| 34 |
+
const index = new Map<string, FamilyAppearance[]>()
|
| 35 |
+
if (!hierarchy?.families) {
|
| 36 |
+
return index
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
for (const family of hierarchy.families) {
|
| 40 |
+
for (const evalSummaryId of family.eval_summary_ids ?? []) {
|
| 41 |
+
const composite = findComposite(family, evalSummaryId)
|
| 42 |
+
const list = index.get(evalSummaryId) ?? []
|
| 43 |
+
list.push({ family, composite })
|
| 44 |
+
index.set(evalSummaryId, list)
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
return index
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
/**
|
| 52 |
+
* Build a lookup that maps each `eval_summary_id` to the family / composite
|
| 53 |
+
* that contains it in `hierarchy.json`. The model-detail benchmark grouping
|
| 54 |
+
* needs this because the eval row's own `family_id` is null for ~7% of evals
|
| 55 |
+
* (e.g. CySE2 composites) and points at the leaf instead of the parent for
|
| 56 |
+
* ~70% (singleton families). The hierarchy is the only source that captures
|
| 57 |
+
* curated family→composite→benchmark grouping.
|
| 58 |
+
*
|
| 59 |
+
* 31 eval_summary_ids appear in multiple families. The optional
|
| 60 |
+
* `preferFamilyKey(evalSummaryId)` callback lets the caller pick the canonical
|
| 61 |
+
* family — typically by passing in the eval row's own `family_id`. When no
|
| 62 |
+
* preference is given, the first family encountered wins.
|
| 63 |
+
*/
|
| 64 |
+
export function buildHierarchyEvalIndex(
|
| 65 |
+
hierarchy: EvalHierarchy | null | undefined,
|
| 66 |
+
preferFamilyKey?: (evalSummaryId: string) => string | null | undefined,
|
| 67 |
+
): Map<string, HierarchyEvalLocation> {
|
| 68 |
+
const appearances = buildAppearancesIndex(hierarchy)
|
| 69 |
+
const index = new Map<string, HierarchyEvalLocation>()
|
| 70 |
+
|
| 71 |
+
for (const [evalSummaryId, candidates] of appearances) {
|
| 72 |
+
let chosen = candidates[0]
|
| 73 |
+
if (candidates.length > 1) {
|
| 74 |
+
const preferredKey = preferFamilyKey?.(evalSummaryId)?.toString().trim()
|
| 75 |
+
if (preferredKey) {
|
| 76 |
+
const match = candidates.find((c) => c.family.key === preferredKey)
|
| 77 |
+
if (match) {
|
| 78 |
+
chosen = match
|
| 79 |
+
}
|
| 80 |
+
}
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
index.set(evalSummaryId, {
|
| 84 |
+
familyKey: chosen.family.key,
|
| 85 |
+
familyDisplayName: chosen.family.display_name,
|
| 86 |
+
compositeKey: chosen.composite?.key,
|
| 87 |
+
compositeDisplayName: chosen.composite?.display_name,
|
| 88 |
+
})
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
return index
|
| 92 |
+
}
|