Spaces:
Running
Running
Untrack accidentally-committed noise files; gitignore the patterns
Browse filesThe previous commit (b6dab21) pulled in several files that should not
be tracked:
- "(1).tsx" — Finder duplicates of route files
- output/hierarchy_explorer.html — local probe output
- public/peer-ranks.json — local-generated cache (~370k lines)
- app/embed/eval/distribution/[...id]/page.tsx — orphan route
Remove from the index and extend .gitignore so they can't sneak back
through a `git add -A` or accidental wildcard.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- .gitignore +3 -0
- app/developers/[...id]/page (1).tsx +0 -244
- app/embed/eval/distribution/[...id]/page.tsx +0 -228
- app/evals/[...id]/page (1).tsx +0 -734
- app/models/[...id]/page (1).tsx +0 -355
- output/hierarchy_explorer.html +0 -0
- public/peer-ranks.json +0 -0
.gitignore
CHANGED
|
@@ -46,3 +46,6 @@ pipeline_revised.py
|
|
| 46 |
mock_design/
|
| 47 |
shoot.mjs
|
| 48 |
whisker-render.mjs
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
mock_design/
|
| 47 |
shoot.mjs
|
| 48 |
whisker-render.mjs
|
| 49 |
+
output/
|
| 50 |
+
public/peer-ranks.json
|
| 51 |
+
**/* (1).tsx
|
app/developers/[...id]/page (1).tsx
DELETED
|
@@ -1,244 +0,0 @@
|
|
| 1 |
-
"use client"
|
| 2 |
-
|
| 3 |
-
import { useCallback, useEffect, useMemo, useState } from "react"
|
| 4 |
-
import { useParams, useRouter } from "next/navigation"
|
| 5 |
-
import { ArrowLeft, ArrowUpDown, Search, Tag } from "lucide-react"
|
| 6 |
-
|
| 7 |
-
import { BenchmarkEvaluationCard, type BenchmarkEvaluationCardData } from "@/components/benchmark-evaluation-card"
|
| 8 |
-
import { ListPagination } from "@/components/list-pagination"
|
| 9 |
-
import { Navigation } from "@/components/navigation"
|
| 10 |
-
import { PageHeader } from "@/components/page-header"
|
| 11 |
-
import { Button } from "@/components/ui/button"
|
| 12 |
-
import { Input } from "@/components/ui/input"
|
| 13 |
-
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
| 14 |
-
import type { BenchmarkCard } from "@/lib/benchmark-schema"
|
| 15 |
-
import { lookupBenchmarkCard } from "@/lib/benchmark-metadata-utils"
|
| 16 |
-
import { fetchDeveloperSummary, fetchBenchmarkMetadata } from "@/lib/dashboard-data-client"
|
| 17 |
-
|
| 18 |
-
const PAGE_SIZE = 40
|
| 19 |
-
|
| 20 |
-
export default function DeveloperDetailPage() {
|
| 21 |
-
const params = useParams()
|
| 22 |
-
const router = useRouter()
|
| 23 |
-
const [developer, setDeveloper] = useState<string>("")
|
| 24 |
-
const [models, setModels] = useState<BenchmarkEvaluationCardData[]>([])
|
| 25 |
-
const [benchmarkCards, setBenchmarkCards] = useState<Record<string, BenchmarkCard>>({})
|
| 26 |
-
const [loading, setLoading] = useState(true)
|
| 27 |
-
const [error, setError] = useState<string | null>(null)
|
| 28 |
-
const [searchQuery, setSearchQuery] = useState("")
|
| 29 |
-
const [sortBy, setSortBy] = useState<"date" | "name" | "benchmarks">("date")
|
| 30 |
-
const [page, setPage] = useState(1)
|
| 31 |
-
|
| 32 |
-
const routeId = params.id as string
|
| 33 |
-
|
| 34 |
-
const handleBack = useCallback(() => {
|
| 35 |
-
router.push("/developers")
|
| 36 |
-
}, [router])
|
| 37 |
-
|
| 38 |
-
useEffect(() => {
|
| 39 |
-
Promise.all([
|
| 40 |
-
fetchDeveloperSummary(routeId),
|
| 41 |
-
fetchBenchmarkMetadata(),
|
| 42 |
-
])
|
| 43 |
-
.then(([summary, cards]) => {
|
| 44 |
-
setDeveloper(summary.developer)
|
| 45 |
-
setModels(summary.models)
|
| 46 |
-
setBenchmarkCards(cards)
|
| 47 |
-
})
|
| 48 |
-
.catch((err) => {
|
| 49 |
-
console.error(err)
|
| 50 |
-
setError("Developer not found")
|
| 51 |
-
})
|
| 52 |
-
.finally(() => setLoading(false))
|
| 53 |
-
}, [routeId])
|
| 54 |
-
|
| 55 |
-
// Collect all unique domains from benchmarks this developer's models are evaluated on
|
| 56 |
-
const domainCoverage = useMemo(() => {
|
| 57 |
-
const domainMap = new Map<string, Set<string>>() // domain → set of benchmark names
|
| 58 |
-
for (const model of models) {
|
| 59 |
-
for (const { benchmark } of model.top_scores) {
|
| 60 |
-
const card = lookupBenchmarkCard(benchmarkCards, benchmark)
|
| 61 |
-
for (const domain of card?.benchmark_details?.domains ?? []) {
|
| 62 |
-
const existing = domainMap.get(domain) ?? new Set()
|
| 63 |
-
existing.add(benchmark)
|
| 64 |
-
domainMap.set(domain, existing)
|
| 65 |
-
}
|
| 66 |
-
}
|
| 67 |
-
}
|
| 68 |
-
return Array.from(domainMap.entries())
|
| 69 |
-
.map(([domain, benchmarks]) => ({ domain, count: benchmarks.size }))
|
| 70 |
-
.sort((a, b) => b.count - a.count)
|
| 71 |
-
}, [models, benchmarkCards])
|
| 72 |
-
|
| 73 |
-
const filteredModels = useMemo(() => {
|
| 74 |
-
const query = searchQuery.trim().toLowerCase()
|
| 75 |
-
const filtered = query
|
| 76 |
-
? models.filter((model) => {
|
| 77 |
-
const haystacks = [
|
| 78 |
-
model.model_name,
|
| 79 |
-
model.canonical_model_name,
|
| 80 |
-
model.developer,
|
| 81 |
-
...model.top_scores.map((score) => score.benchmark),
|
| 82 |
-
]
|
| 83 |
-
|
| 84 |
-
return haystacks.some((value) => value?.toLowerCase().includes(query))
|
| 85 |
-
})
|
| 86 |
-
: [...models]
|
| 87 |
-
|
| 88 |
-
switch (sortBy) {
|
| 89 |
-
case "date":
|
| 90 |
-
filtered.sort(
|
| 91 |
-
(a, b) =>
|
| 92 |
-
new Date(b.latest_timestamp).getTime() -
|
| 93 |
-
new Date(a.latest_timestamp).getTime()
|
| 94 |
-
)
|
| 95 |
-
break
|
| 96 |
-
case "name":
|
| 97 |
-
filtered.sort((a, b) => a.model_name.localeCompare(b.model_name))
|
| 98 |
-
break
|
| 99 |
-
case "benchmarks":
|
| 100 |
-
filtered.sort((a, b) => b.benchmarks_count - a.benchmarks_count)
|
| 101 |
-
break
|
| 102 |
-
}
|
| 103 |
-
|
| 104 |
-
return filtered
|
| 105 |
-
}, [models, searchQuery, sortBy])
|
| 106 |
-
|
| 107 |
-
useEffect(() => {
|
| 108 |
-
setPage(1)
|
| 109 |
-
}, [searchQuery, sortBy])
|
| 110 |
-
|
| 111 |
-
const pagedModels = useMemo(
|
| 112 |
-
() => filteredModels.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE),
|
| 113 |
-
[filteredModels, page]
|
| 114 |
-
)
|
| 115 |
-
|
| 116 |
-
if (loading) {
|
| 117 |
-
return (
|
| 118 |
-
<div className="min-h-screen bg-background">
|
| 119 |
-
<Navigation />
|
| 120 |
-
<main className="container mx-auto px-4 py-8">
|
| 121 |
-
<div className="flex h-96 items-center justify-center">
|
| 122 |
-
<div className="text-lg text-muted-foreground">Loading developer...</div>
|
| 123 |
-
</div>
|
| 124 |
-
</main>
|
| 125 |
-
</div>
|
| 126 |
-
)
|
| 127 |
-
}
|
| 128 |
-
|
| 129 |
-
if (error) {
|
| 130 |
-
return (
|
| 131 |
-
<div className="min-h-screen bg-background">
|
| 132 |
-
<Navigation />
|
| 133 |
-
<main className="container mx-auto px-4 py-8">
|
| 134 |
-
<div className="flex flex-col items-center justify-center h-96 space-y-4">
|
| 135 |
-
<div className="text-lg text-muted-foreground">{error}</div>
|
| 136 |
-
<Button onClick={handleBack}>
|
| 137 |
-
<ArrowLeft className="mr-2 h-4 w-4" />
|
| 138 |
-
Back to Developers
|
| 139 |
-
</Button>
|
| 140 |
-
</div>
|
| 141 |
-
</main>
|
| 142 |
-
</div>
|
| 143 |
-
)
|
| 144 |
-
}
|
| 145 |
-
|
| 146 |
-
return (
|
| 147 |
-
<div className="min-h-screen bg-background">
|
| 148 |
-
<Navigation />
|
| 149 |
-
<main className="container mx-auto px-4 py-8">
|
| 150 |
-
<PageHeader
|
| 151 |
-
eyebrow="Developer"
|
| 152 |
-
title={developer}
|
| 153 |
-
description={`Evaluation coverage across ${models.length} model${models.length !== 1 ? "s" : ""} from this developer, including benchmark domain coverage where metadata is available.`}
|
| 154 |
-
metaItems={[
|
| 155 |
-
{ label: "Models", value: models.length.toString() },
|
| 156 |
-
{
|
| 157 |
-
label: "Reported Results",
|
| 158 |
-
value: models.reduce((sum, model) => sum + model.evaluations_count, 0).toString(),
|
| 159 |
-
},
|
| 160 |
-
...(domainCoverage.length > 0
|
| 161 |
-
? [{ label: "Domains covered", value: domainCoverage.length.toString() }]
|
| 162 |
-
: []),
|
| 163 |
-
]}
|
| 164 |
-
>
|
| 165 |
-
<Button variant="outline" onClick={handleBack}>
|
| 166 |
-
<ArrowLeft className="mr-2 h-4 w-4" />
|
| 167 |
-
Back
|
| 168 |
-
</Button>
|
| 169 |
-
</PageHeader>
|
| 170 |
-
|
| 171 |
-
{/* Domain coverage strip */}
|
| 172 |
-
{domainCoverage.length > 0 && (
|
| 173 |
-
<div className="mb-4 mt-6 rounded-[1.5rem] border border-border/70 bg-muted/10 p-4">
|
| 174 |
-
<div className="mb-2 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.2em] text-muted-foreground">
|
| 175 |
-
<Tag className="h-3.5 w-3.5" />
|
| 176 |
-
Benchmark domain coverage
|
| 177 |
-
</div>
|
| 178 |
-
<div className="flex flex-wrap gap-2">
|
| 179 |
-
{domainCoverage.map(({ domain, count }) => (
|
| 180 |
-
<span
|
| 181 |
-
key={domain}
|
| 182 |
-
className="inline-flex items-center gap-1.5 rounded-full border border-border/60 bg-background px-3 py-1 text-xs font-medium capitalize"
|
| 183 |
-
>
|
| 184 |
-
{domain}
|
| 185 |
-
<span className="rounded-full bg-muted px-1.5 py-0.5 text-[10px] font-semibold text-muted-foreground">
|
| 186 |
-
{count}
|
| 187 |
-
</span>
|
| 188 |
-
</span>
|
| 189 |
-
))}
|
| 190 |
-
</div>
|
| 191 |
-
</div>
|
| 192 |
-
)}
|
| 193 |
-
|
| 194 |
-
<div className="mb-8 mt-8 flex flex-col gap-4 border-b border-border/50 pb-6 sm:flex-row">
|
| 195 |
-
<div className="relative w-full sm:max-w-sm">
|
| 196 |
-
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
| 197 |
-
<Input
|
| 198 |
-
value={searchQuery}
|
| 199 |
-
onChange={(event) => setSearchQuery(event.target.value)}
|
| 200 |
-
placeholder="Search models or benchmarks"
|
| 201 |
-
className="pl-9"
|
| 202 |
-
/>
|
| 203 |
-
</div>
|
| 204 |
-
<Select value={sortBy} onValueChange={(value) => setSortBy(value as typeof sortBy)}>
|
| 205 |
-
<SelectTrigger className="w-[180px]">
|
| 206 |
-
<ArrowUpDown className="mr-2 h-4 w-4" />
|
| 207 |
-
<SelectValue placeholder="Sort by" />
|
| 208 |
-
</SelectTrigger>
|
| 209 |
-
<SelectContent>
|
| 210 |
-
<SelectItem value="date">Latest First</SelectItem>
|
| 211 |
-
<SelectItem value="name">Name (A-Z)</SelectItem>
|
| 212 |
-
<SelectItem value="benchmarks">Most Benchmark Coverage</SelectItem>
|
| 213 |
-
</SelectContent>
|
| 214 |
-
</Select>
|
| 215 |
-
</div>
|
| 216 |
-
|
| 217 |
-
{filteredModels.length === 0 ? (
|
| 218 |
-
<div className="py-12 text-center text-lg text-muted-foreground">
|
| 219 |
-
No models found matching your filters
|
| 220 |
-
</div>
|
| 221 |
-
) : (
|
| 222 |
-
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-2">
|
| 223 |
-
{pagedModels.map((model, index) => (
|
| 224 |
-
<BenchmarkEvaluationCard
|
| 225 |
-
key={model.id}
|
| 226 |
-
data={model}
|
| 227 |
-
benchmarkCards={benchmarkCards}
|
| 228 |
-
delayMs={Math.min(index * 45, 240)}
|
| 229 |
-
/>
|
| 230 |
-
))}
|
| 231 |
-
</div>
|
| 232 |
-
)}
|
| 233 |
-
|
| 234 |
-
<ListPagination
|
| 235 |
-
page={page}
|
| 236 |
-
pageSize={PAGE_SIZE}
|
| 237 |
-
totalItems={filteredModels.length}
|
| 238 |
-
itemLabel="models"
|
| 239 |
-
onPageChange={setPage}
|
| 240 |
-
/>
|
| 241 |
-
</main>
|
| 242 |
-
</div>
|
| 243 |
-
)
|
| 244 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/embed/eval/distribution/[...id]/page.tsx
DELETED
|
@@ -1,228 +0,0 @@
|
|
| 1 |
-
"use client"
|
| 2 |
-
|
| 3 |
-
import { useEffect, useMemo, useState } from "react"
|
| 4 |
-
import { useParams, useSearchParams } from "next/navigation"
|
| 5 |
-
import { ScoreDistribution } from "@/components/score-distribution"
|
| 6 |
-
import { fetchEvalSummary } from "@/lib/dashboard-data-client"
|
| 7 |
-
import { getMetricChipLabel } from "@/lib/metric-labels"
|
| 8 |
-
import { routeIdFromSegments } from "@/lib/utils"
|
| 9 |
-
import type { BenchmarkEvalSummary } from "@/lib/eval-processing"
|
| 10 |
-
|
| 11 |
-
/**
|
| 12 |
-
* Embed-only render of the score-distribution histogram for one eval.
|
| 13 |
-
* Designed for iframes: no nav, no audience bar, no surrounding chrome.
|
| 14 |
-
*
|
| 15 |
-
* Slice-aware: when the eval has subtask slices (e.g. Global MMLU's
|
| 16 |
-
* per-language splits), this page renders a SPLIT dropdown above the
|
| 17 |
-
* plot so the embedded viewer can switch slices the same way the parent
|
| 18 |
-
* eval page does. When there's no slice axis, falls back to building
|
| 19 |
-
* one series per metric (e.g. agentharm's per-category histogram).
|
| 20 |
-
*
|
| 21 |
-
* Query params:
|
| 22 |
-
* ?view=distribution (default) — lock to distribution, hide toggle
|
| 23 |
-
* ?view=frontier — lock to frontier, hide toggle
|
| 24 |
-
* ?view=both — show the Distribution/Frontier toggle
|
| 25 |
-
* ?slice=<subtask_key> — start with this slice selected
|
| 26 |
-
*/
|
| 27 |
-
export default function EmbedEvalDistribution() {
|
| 28 |
-
const params = useParams()
|
| 29 |
-
const searchParams = useSearchParams()
|
| 30 |
-
const evalId = routeIdFromSegments(params.id)
|
| 31 |
-
const viewParam = (searchParams.get("view") || "distribution").toLowerCase()
|
| 32 |
-
const sliceParam = searchParams.get("slice")
|
| 33 |
-
const showToggle = viewParam === "both"
|
| 34 |
-
const defaultView: "distribution" | "frontier" =
|
| 35 |
-
viewParam === "frontier" ? "frontier" : "distribution"
|
| 36 |
-
|
| 37 |
-
const [summary, setSummary] = useState<BenchmarkEvalSummary | null>(null)
|
| 38 |
-
const [error, setError] = useState<string | null>(null)
|
| 39 |
-
|
| 40 |
-
useEffect(() => {
|
| 41 |
-
let cancelled = false
|
| 42 |
-
fetchEvalSummary(evalId)
|
| 43 |
-
.then((s) => {
|
| 44 |
-
if (!cancelled) setSummary(s)
|
| 45 |
-
})
|
| 46 |
-
.catch((err) => {
|
| 47 |
-
if (!cancelled) setError(err instanceof Error ? err.message : String(err))
|
| 48 |
-
})
|
| 49 |
-
return () => {
|
| 50 |
-
cancelled = true
|
| 51 |
-
}
|
| 52 |
-
}, [evalId])
|
| 53 |
-
|
| 54 |
-
// Slice axis — present when the eval has multiple subtask-scope metrics
|
| 55 |
-
// sharing a primary root metric (e.g. Global MMLU's 24 language splits).
|
| 56 |
-
const sliceAxis = useMemo(() => {
|
| 57 |
-
if (!summary) return null
|
| 58 |
-
const metrics = summary.leaderboard_metrics ?? []
|
| 59 |
-
const primary = metrics.find((m) => m.scope !== "subtask")
|
| 60 |
-
if (!primary?.column_key) return null
|
| 61 |
-
const seen = new Map<string, string>()
|
| 62 |
-
for (const m of metrics) {
|
| 63 |
-
if (m.scope === "subtask" && m.subtask_key && !seen.has(m.subtask_key)) {
|
| 64 |
-
seen.set(m.subtask_key, m.subtask_name ?? m.subtask_key)
|
| 65 |
-
}
|
| 66 |
-
}
|
| 67 |
-
if (seen.size <= 1) return null
|
| 68 |
-
return {
|
| 69 |
-
primaryColumn: primary.column_key,
|
| 70 |
-
primaryLabel: getMetricChipLabel(primary),
|
| 71 |
-
unit: primary.unit ?? summary.metric_config.unit,
|
| 72 |
-
lowerIsBetter: Boolean(primary.lower_is_better ?? summary.metric_config.lower_is_better),
|
| 73 |
-
slices: Array.from(seen, ([key, label]) => ({ key, label })),
|
| 74 |
-
}
|
| 75 |
-
}, [summary])
|
| 76 |
-
|
| 77 |
-
const ALL_SLICE_KEY = "__all__"
|
| 78 |
-
const [activeSlice, setActiveSlice] = useState<string>(() => {
|
| 79 |
-
if (sliceParam && sliceParam.trim()) return sliceParam.trim()
|
| 80 |
-
return ALL_SLICE_KEY
|
| 81 |
-
})
|
| 82 |
-
// If the URL named a slice the eval doesn't carry, fall back to Overall
|
| 83 |
-
// once the summary loads.
|
| 84 |
-
useEffect(() => {
|
| 85 |
-
if (!sliceAxis) return
|
| 86 |
-
if (activeSlice === ALL_SLICE_KEY) return
|
| 87 |
-
if (!sliceAxis.slices.some((s) => s.key === activeSlice)) {
|
| 88 |
-
setActiveSlice(ALL_SLICE_KEY)
|
| 89 |
-
}
|
| 90 |
-
}, [activeSlice, sliceAxis])
|
| 91 |
-
|
| 92 |
-
const series = useMemo(() => {
|
| 93 |
-
if (!summary) return null
|
| 94 |
-
const rows = summary.leaderboard_rows ?? []
|
| 95 |
-
|
| 96 |
-
// Slice-axis path: render one series for the active slice (Overall or
|
| 97 |
-
// a specific subtask). Drives the SPLIT dropdown UX.
|
| 98 |
-
if (sliceAxis) {
|
| 99 |
-
const columnKey =
|
| 100 |
-
activeSlice === ALL_SLICE_KEY
|
| 101 |
-
? sliceAxis.primaryColumn
|
| 102 |
-
: `${sliceAxis.primaryColumn}::${activeSlice}`
|
| 103 |
-
const points: { score: number; releaseDate: string | null; modelName: string }[] = []
|
| 104 |
-
for (const row of rows) {
|
| 105 |
-
const raw = (row.values as Record<string, unknown> | undefined)?.[columnKey]
|
| 106 |
-
const numeric = typeof raw === "number" ? raw : Number(raw)
|
| 107 |
-
if (!Number.isFinite(numeric)) continue
|
| 108 |
-
const modelInfo = (row as { model_info?: { name?: string; release_date?: string | null } }).model_info
|
| 109 |
-
points.push({
|
| 110 |
-
score: numeric,
|
| 111 |
-
modelName: modelInfo?.name ?? "",
|
| 112 |
-
releaseDate: modelInfo?.release_date ?? null,
|
| 113 |
-
})
|
| 114 |
-
}
|
| 115 |
-
if (points.length < 3) return null
|
| 116 |
-
const sliceLabel =
|
| 117 |
-
activeSlice === ALL_SLICE_KEY
|
| 118 |
-
? "Overall"
|
| 119 |
-
: sliceAxis.slices.find((s) => s.key === activeSlice)?.label ?? activeSlice
|
| 120 |
-
return [
|
| 121 |
-
{
|
| 122 |
-
key: `${sliceAxis.primaryColumn}::${activeSlice}`,
|
| 123 |
-
label: `${sliceAxis.primaryLabel} · ${sliceLabel}`,
|
| 124 |
-
values: points.map((p) => p.score),
|
| 125 |
-
unit: sliceAxis.unit,
|
| 126 |
-
lowerIsBetter: sliceAxis.lowerIsBetter,
|
| 127 |
-
points,
|
| 128 |
-
},
|
| 129 |
-
]
|
| 130 |
-
}
|
| 131 |
-
|
| 132 |
-
// Non-slice path: one series per metric (e.g. agentharm's multi-metric
|
| 133 |
-
// histogram). ScoreDistribution surfaces a metric chip picker.
|
| 134 |
-
const metrics = summary.leaderboard_metrics ?? []
|
| 135 |
-
const built = metrics
|
| 136 |
-
.map((metric) => {
|
| 137 |
-
const columnKey = metric.column_key ?? metric.metric_summary_id
|
| 138 |
-
if (!columnKey) return null
|
| 139 |
-
const points: { score: number; releaseDate: string | null; modelName: string }[] = []
|
| 140 |
-
for (const row of rows) {
|
| 141 |
-
const raw = (row.values as Record<string, unknown> | undefined)?.[columnKey]
|
| 142 |
-
const numeric = typeof raw === "number" ? raw : Number(raw)
|
| 143 |
-
if (!Number.isFinite(numeric)) continue
|
| 144 |
-
const modelInfo = (row as { model_info?: { name?: string; release_date?: string | null } }).model_info
|
| 145 |
-
points.push({
|
| 146 |
-
score: numeric,
|
| 147 |
-
modelName: modelInfo?.name ?? "",
|
| 148 |
-
releaseDate: modelInfo?.release_date ?? null,
|
| 149 |
-
})
|
| 150 |
-
}
|
| 151 |
-
if (points.length < 3) return null
|
| 152 |
-
return {
|
| 153 |
-
key: columnKey,
|
| 154 |
-
label: getMetricChipLabel(metric),
|
| 155 |
-
values: points.map((p) => p.score),
|
| 156 |
-
unit: metric.unit ?? summary.metric_config.unit,
|
| 157 |
-
lowerIsBetter: Boolean(metric.lower_is_better ?? summary.metric_config.lower_is_better),
|
| 158 |
-
points,
|
| 159 |
-
}
|
| 160 |
-
})
|
| 161 |
-
.filter((s): s is NonNullable<typeof s> => s !== null)
|
| 162 |
-
return built.length > 0 ? built : null
|
| 163 |
-
}, [summary, sliceAxis, activeSlice])
|
| 164 |
-
|
| 165 |
-
if (error) {
|
| 166 |
-
return (
|
| 167 |
-
<div className="font-mono" style={{ fontSize: 12, color: "var(--fg-muted)" }}>
|
| 168 |
-
Failed to load: {error}
|
| 169 |
-
</div>
|
| 170 |
-
)
|
| 171 |
-
}
|
| 172 |
-
if (!summary) {
|
| 173 |
-
return (
|
| 174 |
-
<div
|
| 175 |
-
className="font-mono uppercase"
|
| 176 |
-
style={{ fontSize: 10, letterSpacing: "0.18em", color: "var(--fg-subtle)" }}
|
| 177 |
-
>
|
| 178 |
-
Loading…
|
| 179 |
-
</div>
|
| 180 |
-
)
|
| 181 |
-
}
|
| 182 |
-
if (!series) {
|
| 183 |
-
return (
|
| 184 |
-
<div className="font-mono" style={{ fontSize: 12, color: "var(--fg-muted)" }}>
|
| 185 |
-
No score data available for this evaluation yet.
|
| 186 |
-
</div>
|
| 187 |
-
)
|
| 188 |
-
}
|
| 189 |
-
|
| 190 |
-
return (
|
| 191 |
-
<div>
|
| 192 |
-
<div
|
| 193 |
-
className="font-mono uppercase mb-2"
|
| 194 |
-
style={{ fontSize: 10, letterSpacing: "0.14em", color: "var(--fg-subtle)" }}
|
| 195 |
-
>
|
| 196 |
-
{summary.evaluation_name} ·{" "}
|
| 197 |
-
{defaultView === "frontier" ? "Pareto frontier" : "Score distribution"}
|
| 198 |
-
</div>
|
| 199 |
-
{sliceAxis && (
|
| 200 |
-
<div className="mb-3 flex items-center gap-3">
|
| 201 |
-
<span
|
| 202 |
-
className="font-mono uppercase shrink-0"
|
| 203 |
-
style={{ fontSize: 10, letterSpacing: "0.14em", color: "var(--fg-subtle)" }}
|
| 204 |
-
>
|
| 205 |
-
Split
|
| 206 |
-
</span>
|
| 207 |
-
<select
|
| 208 |
-
className="ec-select"
|
| 209 |
-
value={activeSlice}
|
| 210 |
-
onChange={(e) => setActiveSlice(e.target.value)}
|
| 211 |
-
>
|
| 212 |
-
<option value={ALL_SLICE_KEY}>Overall</option>
|
| 213 |
-
{sliceAxis.slices.map((s) => (
|
| 214 |
-
<option key={s.key} value={s.key}>
|
| 215 |
-
{s.label}
|
| 216 |
-
</option>
|
| 217 |
-
))}
|
| 218 |
-
</select>
|
| 219 |
-
</div>
|
| 220 |
-
)}
|
| 221 |
-
<ScoreDistribution
|
| 222 |
-
series={series}
|
| 223 |
-
defaultView={defaultView}
|
| 224 |
-
showViewToggle={showToggle}
|
| 225 |
-
/>
|
| 226 |
-
</div>
|
| 227 |
-
)
|
| 228 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/evals/[...id]/page (1).tsx
DELETED
|
@@ -1,734 +0,0 @@
|
|
| 1 |
-
"use client"
|
| 2 |
-
|
| 3 |
-
import { useCallback, useEffect, useMemo, useState } from "react"
|
| 4 |
-
import { useParams, usePathname, useRouter, useSearchParams } from "next/navigation"
|
| 5 |
-
import Link from "next/link"
|
| 6 |
-
import { Button } from "@/components/ui/button"
|
| 7 |
-
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
| 8 |
-
import { Badge } from "@/components/ui/badge"
|
| 9 |
-
import { Input } from "@/components/ui/input"
|
| 10 |
-
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
| 11 |
-
import { ArrowLeft, BarChart3, Grid3X3, Search } from "lucide-react"
|
| 12 |
-
import { Navigation } from "@/components/navigation"
|
| 13 |
-
import { EvalDetail } from "@/components/eval-detail"
|
| 14 |
-
import type { BenchmarkEvalSummary } from "@/lib/eval-processing"
|
| 15 |
-
import { fetchEvalSummary } from "@/lib/dashboard-data-client"
|
| 16 |
-
import { getCategoryColor } from "@/lib/benchmark-schema"
|
| 17 |
-
|
| 18 |
-
const PARAM_RANGE_VALUES = [1, 2, 3, 4, 6, 8, 10, 12, 16, 24, 32, 48, 64, 96, 128, 192, 256, 384, 500] as const
|
| 19 |
-
const PARAM_RANGE_MARKERS = [
|
| 20 |
-
{ label: "< 1B", step: 0 },
|
| 21 |
-
{ label: "6B", step: PARAM_RANGE_VALUES.indexOf(6) },
|
| 22 |
-
{ label: "12B", step: PARAM_RANGE_VALUES.indexOf(12) },
|
| 23 |
-
{ label: "32B", step: PARAM_RANGE_VALUES.indexOf(32) },
|
| 24 |
-
{ label: "128B", step: PARAM_RANGE_VALUES.indexOf(128) },
|
| 25 |
-
{ label: "> 500B", step: PARAM_RANGE_VALUES.length - 1 },
|
| 26 |
-
] as const
|
| 27 |
-
|
| 28 |
-
function formatParamBoundLabel(step: number, bound: "min" | "max") {
|
| 29 |
-
const maxStepIndex = PARAM_RANGE_VALUES.length - 1
|
| 30 |
-
if (bound === "min" && step <= 0) return "< 1B"
|
| 31 |
-
if (bound === "max" && step >= maxStepIndex) return "> 500B"
|
| 32 |
-
const value = PARAM_RANGE_VALUES[step]
|
| 33 |
-
return value != null ? `${value}B` : "Not reported"
|
| 34 |
-
}
|
| 35 |
-
|
| 36 |
-
function normalizeMetadataList(value: unknown): string[] {
|
| 37 |
-
if (Array.isArray(value)) {
|
| 38 |
-
return value
|
| 39 |
-
.filter((item): item is string => typeof item === "string")
|
| 40 |
-
.map((item) => item.trim())
|
| 41 |
-
.filter(Boolean)
|
| 42 |
-
}
|
| 43 |
-
|
| 44 |
-
if (typeof value !== "string") return []
|
| 45 |
-
|
| 46 |
-
const normalized = value.trim()
|
| 47 |
-
if (!normalized) return []
|
| 48 |
-
|
| 49 |
-
const looksDelimited = /[,;|]/.test(normalized)
|
| 50 |
-
if (looksDelimited) {
|
| 51 |
-
return normalized
|
| 52 |
-
.split(/[,;|]/)
|
| 53 |
-
.map((item) => item.trim())
|
| 54 |
-
.filter(Boolean)
|
| 55 |
-
}
|
| 56 |
-
|
| 57 |
-
// Treat long prose values as invalid list data rather than rendering oversized chips.
|
| 58 |
-
if (normalized.length > 40 || /\s/.test(normalized)) return []
|
| 59 |
-
|
| 60 |
-
return [normalized]
|
| 61 |
-
}
|
| 62 |
-
|
| 63 |
-
export default function EvalDetailPage() {
|
| 64 |
-
const params = useParams()
|
| 65 |
-
const pathname = usePathname()
|
| 66 |
-
const router = useRouter()
|
| 67 |
-
const searchParams = useSearchParams()
|
| 68 |
-
const [summary, setSummary] = useState<BenchmarkEvalSummary | null>(null)
|
| 69 |
-
const [subSummaries, setSubSummaries] = useState<BenchmarkEvalSummary[]>([])
|
| 70 |
-
const [loading, setLoading] = useState(true)
|
| 71 |
-
const [error, setError] = useState<string | null>(null)
|
| 72 |
-
const [matrixSearch, setMatrixSearch] = useState("")
|
| 73 |
-
const returnTo = searchParams.get("from")
|
| 74 |
-
const currentDetailHref = useMemo(() => {
|
| 75 |
-
const params = new URLSearchParams(searchParams.toString())
|
| 76 |
-
params.delete("from")
|
| 77 |
-
const query = params.toString()
|
| 78 |
-
return query ? `${pathname}?${query}` : pathname
|
| 79 |
-
}, [pathname, searchParams])
|
| 80 |
-
|
| 81 |
-
const handleBack = useCallback(() => {
|
| 82 |
-
if (returnTo?.startsWith("/")) {
|
| 83 |
-
router.push(returnTo)
|
| 84 |
-
return
|
| 85 |
-
}
|
| 86 |
-
|
| 87 |
-
if (typeof window !== "undefined" && window.history.length > 1) {
|
| 88 |
-
router.back()
|
| 89 |
-
return
|
| 90 |
-
}
|
| 91 |
-
|
| 92 |
-
router.push("/evals")
|
| 93 |
-
}, [returnTo, router])
|
| 94 |
-
|
| 95 |
-
useEffect(() => {
|
| 96 |
-
const load = async () => {
|
| 97 |
-
try {
|
| 98 |
-
const evalId = decodeURIComponent(params.id as string)
|
| 99 |
-
const found = await fetchEvalSummary(evalId)
|
| 100 |
-
setSummary(found)
|
| 101 |
-
document.title = `${found.evaluation_name} | Benchmark`
|
| 102 |
-
|
| 103 |
-
// If aggregated, fetch each sub-eval for the matrix view
|
| 104 |
-
if (found.is_aggregated && found.aggregate_sources?.length) {
|
| 105 |
-
const subs = await Promise.all(
|
| 106 |
-
found.aggregate_sources.map(async (source) => {
|
| 107 |
-
try {
|
| 108 |
-
return await fetchEvalSummary(source.evaluation_id)
|
| 109 |
-
} catch {
|
| 110 |
-
return null
|
| 111 |
-
}
|
| 112 |
-
})
|
| 113 |
-
)
|
| 114 |
-
setSubSummaries(subs.filter((s): s is BenchmarkEvalSummary => s !== null))
|
| 115 |
-
}
|
| 116 |
-
} catch (err) {
|
| 117 |
-
console.error(err)
|
| 118 |
-
setError("Evaluation not found")
|
| 119 |
-
} finally {
|
| 120 |
-
setLoading(false)
|
| 121 |
-
}
|
| 122 |
-
}
|
| 123 |
-
load()
|
| 124 |
-
}, [params.id])
|
| 125 |
-
|
| 126 |
-
if (loading) {
|
| 127 |
-
return (
|
| 128 |
-
<div className="min-h-screen bg-background">
|
| 129 |
-
<Navigation />
|
| 130 |
-
<main className="container mx-auto px-4 py-8">
|
| 131 |
-
<div className="flex items-center justify-center h-96">
|
| 132 |
-
<div className="text-lg text-muted-foreground">Loading evaluation details...</div>
|
| 133 |
-
</div>
|
| 134 |
-
</main>
|
| 135 |
-
</div>
|
| 136 |
-
)
|
| 137 |
-
}
|
| 138 |
-
|
| 139 |
-
if (error || !summary) {
|
| 140 |
-
return (
|
| 141 |
-
<div className="min-h-screen bg-background">
|
| 142 |
-
<Navigation />
|
| 143 |
-
<main className="container mx-auto px-4 py-8">
|
| 144 |
-
<div className="flex flex-col items-center justify-center h-96 space-y-4">
|
| 145 |
-
<div className="text-lg text-muted-foreground">{error ?? "Evaluation not found"}</div>
|
| 146 |
-
<Button onClick={handleBack}>
|
| 147 |
-
<ArrowLeft className="mr-2 h-4 w-4" />
|
| 148 |
-
Back to Evaluations
|
| 149 |
-
</Button>
|
| 150 |
-
</div>
|
| 151 |
-
</main>
|
| 152 |
-
</div>
|
| 153 |
-
)
|
| 154 |
-
}
|
| 155 |
-
|
| 156 |
-
const isComposite = summary.is_aggregated && (summary.aggregate_sources?.length ?? 0) > 1
|
| 157 |
-
|
| 158 |
-
return (
|
| 159 |
-
<div className="min-h-screen bg-background">
|
| 160 |
-
<Navigation />
|
| 161 |
-
<div className="border-b bg-muted/30">
|
| 162 |
-
<div className="container mx-auto px-4 sm:px-6 py-4 sm:py-6">
|
| 163 |
-
<div className="hidden sm:grid sm:grid-cols-[auto_1fr_auto] sm:items-center sm:gap-4">
|
| 164 |
-
<Button variant="ghost" onClick={handleBack}>
|
| 165 |
-
<ArrowLeft className="mr-2 h-4 w-4" />
|
| 166 |
-
Back
|
| 167 |
-
</Button>
|
| 168 |
-
<div className="text-center">
|
| 169 |
-
<h2 className="text-xl font-medium tracking-tight text-foreground/90 md:text-2xl">
|
| 170 |
-
{isComposite ? "Suite" : "Single benchmark details"}
|
| 171 |
-
</h2>
|
| 172 |
-
</div>
|
| 173 |
-
<div />
|
| 174 |
-
</div>
|
| 175 |
-
<div className="flex items-center gap-3 sm:hidden">
|
| 176 |
-
<Button variant="ghost" size="sm" onClick={handleBack} className="shrink-0">
|
| 177 |
-
<ArrowLeft className="h-4 w-4" />
|
| 178 |
-
</Button>
|
| 179 |
-
<div className="flex-1 text-center">
|
| 180 |
-
<h2 className="text-base font-medium tracking-tight text-foreground/90">
|
| 181 |
-
{isComposite ? "Suite" : "Single benchmark details"}
|
| 182 |
-
</h2>
|
| 183 |
-
</div>
|
| 184 |
-
</div>
|
| 185 |
-
</div>
|
| 186 |
-
</div>
|
| 187 |
-
<main className="container mx-auto px-4 py-8">
|
| 188 |
-
{isComposite ? (
|
| 189 |
-
<CompositeEvalView
|
| 190 |
-
summary={summary}
|
| 191 |
-
subSummaries={subSummaries}
|
| 192 |
-
matrixSearch={matrixSearch}
|
| 193 |
-
onMatrixSearchChange={setMatrixSearch}
|
| 194 |
-
currentDetailHref={currentDetailHref}
|
| 195 |
-
/>
|
| 196 |
-
) : (
|
| 197 |
-
<EvalDetail summary={summary} />
|
| 198 |
-
)}
|
| 199 |
-
</main>
|
| 200 |
-
</div>
|
| 201 |
-
)
|
| 202 |
-
}
|
| 203 |
-
|
| 204 |
-
// ---------------------------------------------------------------------------
|
| 205 |
-
// Suite view (paper §3.2 — composite reporting unit)
|
| 206 |
-
// ---------------------------------------------------------------------------
|
| 207 |
-
|
| 208 |
-
function CompositeEvalView({
|
| 209 |
-
summary,
|
| 210 |
-
subSummaries,
|
| 211 |
-
matrixSearch,
|
| 212 |
-
onMatrixSearchChange,
|
| 213 |
-
currentDetailHref,
|
| 214 |
-
}: {
|
| 215 |
-
summary: BenchmarkEvalSummary
|
| 216 |
-
subSummaries: BenchmarkEvalSummary[]
|
| 217 |
-
matrixSearch: string
|
| 218 |
-
onMatrixSearchChange: (v: string) => void
|
| 219 |
-
currentDetailHref: string
|
| 220 |
-
}) {
|
| 221 |
-
return (
|
| 222 |
-
<div className="space-y-6">
|
| 223 |
-
{/* Header */}
|
| 224 |
-
<Card>
|
| 225 |
-
<CardContent className="p-5 sm:p-6 space-y-4">
|
| 226 |
-
<div className="flex flex-wrap items-center gap-2">
|
| 227 |
-
<Badge variant="outline" className="text-[11px] uppercase tracking-[0.18em]">
|
| 228 |
-
Suite
|
| 229 |
-
</Badge>
|
| 230 |
-
<Badge variant="secondary">
|
| 231 |
-
{summary.aggregate_sources?.length ?? 0} metrics
|
| 232 |
-
</Badge>
|
| 233 |
-
<Badge variant="secondary">{summary.models_count.toLocaleString()} models</Badge>
|
| 234 |
-
<Badge className={getCategoryColor(summary.category)}>
|
| 235 |
-
{summary.category}
|
| 236 |
-
</Badge>
|
| 237 |
-
</div>
|
| 238 |
-
<div>
|
| 239 |
-
<h1 className="text-2xl font-semibold tracking-tight sm:text-3xl">
|
| 240 |
-
{summary.evaluation_name}
|
| 241 |
-
</h1>
|
| 242 |
-
<p className="mt-2 max-w-3xl text-sm leading-6 text-muted-foreground">
|
| 243 |
-
{summary.benchmark_card?.purpose_and_intended_users?.goal
|
| 244 |
-
?? `Suite aggregating ${summary.aggregate_sources?.length ?? 0} metrics across ${summary.models_count.toLocaleString()} models.`}
|
| 245 |
-
</p>
|
| 246 |
-
</div>
|
| 247 |
-
</CardContent>
|
| 248 |
-
</Card>
|
| 249 |
-
|
| 250 |
-
<Tabs defaultValue="metrics">
|
| 251 |
-
<TabsList>
|
| 252 |
-
<TabsTrigger value="metrics" className="gap-2">
|
| 253 |
-
<BarChart3 className="h-4 w-4" />
|
| 254 |
-
Sub-Benchmarks
|
| 255 |
-
</TabsTrigger>
|
| 256 |
-
<TabsTrigger value="matrix" className="gap-2">
|
| 257 |
-
<Grid3X3 className="h-4 w-4" />
|
| 258 |
-
Score breakdown
|
| 259 |
-
</TabsTrigger>
|
| 260 |
-
</TabsList>
|
| 261 |
-
|
| 262 |
-
<TabsContent value="metrics" className="mt-6">
|
| 263 |
-
<SubBenchmarkCards
|
| 264 |
-
sources={summary.aggregate_sources ?? []}
|
| 265 |
-
subSummaries={subSummaries}
|
| 266 |
-
currentDetailHref={currentDetailHref}
|
| 267 |
-
/>
|
| 268 |
-
</TabsContent>
|
| 269 |
-
|
| 270 |
-
<TabsContent value="matrix" className="mt-6">
|
| 271 |
-
<MatrixLeaderboard
|
| 272 |
-
summary={summary}
|
| 273 |
-
subSummaries={subSummaries}
|
| 274 |
-
search={matrixSearch}
|
| 275 |
-
onSearchChange={onMatrixSearchChange}
|
| 276 |
-
/>
|
| 277 |
-
</TabsContent>
|
| 278 |
-
</Tabs>
|
| 279 |
-
</div>
|
| 280 |
-
)
|
| 281 |
-
}
|
| 282 |
-
|
| 283 |
-
// ---------------------------------------------------------------------------
|
| 284 |
-
// Sub-benchmark cards
|
| 285 |
-
// ---------------------------------------------------------------------------
|
| 286 |
-
|
| 287 |
-
function SubBenchmarkCards({
|
| 288 |
-
sources,
|
| 289 |
-
subSummaries,
|
| 290 |
-
currentDetailHref,
|
| 291 |
-
}: {
|
| 292 |
-
sources: NonNullable<BenchmarkEvalSummary["aggregate_sources"]>
|
| 293 |
-
subSummaries: BenchmarkEvalSummary[]
|
| 294 |
-
currentDetailHref: string
|
| 295 |
-
}) {
|
| 296 |
-
const subMap = useMemo(
|
| 297 |
-
() => new Map(subSummaries.map((s) => [s.evaluation_id, s])),
|
| 298 |
-
[subSummaries]
|
| 299 |
-
)
|
| 300 |
-
|
| 301 |
-
return (
|
| 302 |
-
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
| 303 |
-
{sources.map((source) => {
|
| 304 |
-
const sub = subMap.get(source.evaluation_id)
|
| 305 |
-
const card = sub?.benchmark_card
|
| 306 |
-
const overview = card?.benchmark_details?.overview ?? sub?.metric_config?.evaluation_description
|
| 307 |
-
const domains = normalizeMetadataList(card?.benchmark_details?.domains)
|
| 308 |
-
const goal = card?.purpose_and_intended_users?.goal
|
| 309 |
-
|
| 310 |
-
return (
|
| 311 |
-
<Link
|
| 312 |
-
key={source.evaluation_id}
|
| 313 |
-
href={`/evals/${source.evaluation_id}?from=${encodeURIComponent(currentDetailHref)}`}
|
| 314 |
-
className="group"
|
| 315 |
-
>
|
| 316 |
-
<Card className="h-full transition-all hover:-translate-y-0.5 hover:shadow-lg">
|
| 317 |
-
<CardHeader className="pb-3">
|
| 318 |
-
<div className="flex items-center justify-between gap-2">
|
| 319 |
-
<CardTitle className="text-base font-semibold transition-colors group-hover:text-primary">
|
| 320 |
-
{card?.benchmark_details?.name ?? source.composite_benchmark_name}
|
| 321 |
-
</CardTitle>
|
| 322 |
-
<Badge variant="outline" className="text-xs shrink-0">
|
| 323 |
-
{source.models_count} models
|
| 324 |
-
</Badge>
|
| 325 |
-
</div>
|
| 326 |
-
</CardHeader>
|
| 327 |
-
<CardContent className="pt-0 space-y-3">
|
| 328 |
-
{overview && (
|
| 329 |
-
<p className="text-sm text-muted-foreground line-clamp-3">
|
| 330 |
-
{overview}
|
| 331 |
-
</p>
|
| 332 |
-
)}
|
| 333 |
-
|
| 334 |
-
{sub?.best_model && (
|
| 335 |
-
<div className="text-sm">
|
| 336 |
-
<span className="text-muted-foreground">Top: </span>
|
| 337 |
-
<span className="font-medium">{sub.best_model.name}</span>
|
| 338 |
-
<span className="ml-1 text-muted-foreground">
|
| 339 |
-
({(sub.best_model.score * 100).toFixed(1)}%)
|
| 340 |
-
</span>
|
| 341 |
-
</div>
|
| 342 |
-
)}
|
| 343 |
-
|
| 344 |
-
{domains.length > 0 && (
|
| 345 |
-
<div className="flex flex-wrap gap-1.5">
|
| 346 |
-
{domains.slice(0, 3).map((domain) => (
|
| 347 |
-
<span
|
| 348 |
-
key={domain}
|
| 349 |
-
className="rounded-full border border-border/60 bg-muted/30 px-2 py-0.5 text-[10px] font-medium capitalize text-muted-foreground"
|
| 350 |
-
>
|
| 351 |
-
{domain}
|
| 352 |
-
</span>
|
| 353 |
-
))}
|
| 354 |
-
{domains.length > 3 && (
|
| 355 |
-
<span className="rounded-full border border-border/60 bg-muted/30 px-2 py-0.5 text-[10px] text-muted-foreground">
|
| 356 |
-
+{domains.length - 3}
|
| 357 |
-
</span>
|
| 358 |
-
)}
|
| 359 |
-
</div>
|
| 360 |
-
)}
|
| 361 |
-
|
| 362 |
-
<div className="flex flex-wrap items-center gap-1.5">
|
| 363 |
-
{sub?.category && (
|
| 364 |
-
<Badge className={`${getCategoryColor(sub.category)} text-[10px]`}>
|
| 365 |
-
{sub.category}
|
| 366 |
-
</Badge>
|
| 367 |
-
)}
|
| 368 |
-
</div>
|
| 369 |
-
</CardContent>
|
| 370 |
-
</Card>
|
| 371 |
-
</Link>
|
| 372 |
-
)
|
| 373 |
-
})}
|
| 374 |
-
</div>
|
| 375 |
-
)
|
| 376 |
-
}
|
| 377 |
-
|
| 378 |
-
// ---------------------------------------------------------------------------
|
| 379 |
-
// Matrix leaderboard (models × metrics)
|
| 380 |
-
// ---------------------------------------------------------------------------
|
| 381 |
-
|
| 382 |
-
function MatrixLeaderboard({
|
| 383 |
-
summary,
|
| 384 |
-
subSummaries,
|
| 385 |
-
search,
|
| 386 |
-
onSearchChange,
|
| 387 |
-
}: {
|
| 388 |
-
summary: BenchmarkEvalSummary
|
| 389 |
-
subSummaries: BenchmarkEvalSummary[]
|
| 390 |
-
search: string
|
| 391 |
-
onSearchChange: (v: string) => void
|
| 392 |
-
}) {
|
| 393 |
-
const [sortCol, setSortCol] = useState<string>("avg")
|
| 394 |
-
const [sortAsc, setSortAsc] = useState(false)
|
| 395 |
-
const [page, setPage] = useState(1)
|
| 396 |
-
const [hiddenCols, setHiddenCols] = useState<Set<string>>(new Set())
|
| 397 |
-
const [minParamStep, setMinParamStep] = useState(0)
|
| 398 |
-
const [maxParamStep, setMaxParamStep] = useState(PARAM_RANGE_VALUES.length - 1)
|
| 399 |
-
const PAGE_SIZE = 50
|
| 400 |
-
|
| 401 |
-
const metricDirection = useMemo(() => {
|
| 402 |
-
const map = new Map<string, boolean>()
|
| 403 |
-
for (const sub of subSummaries) {
|
| 404 |
-
map.set(sub.evaluation_name, sub.metric_config.lower_is_better)
|
| 405 |
-
}
|
| 406 |
-
return map
|
| 407 |
-
}, [subSummaries])
|
| 408 |
-
|
| 409 |
-
const { models, metrics } = useMemo(() => {
|
| 410 |
-
const metricNames = subSummaries.map((s) => s.evaluation_name)
|
| 411 |
-
const modelScores = new Map<string, { name: string; developer: string; scores: Map<string, number | null> }>()
|
| 412 |
-
|
| 413 |
-
for (const sub of subSummaries) {
|
| 414 |
-
for (const result of sub.model_results) {
|
| 415 |
-
const id = result.model_info.id
|
| 416 |
-
const existing = modelScores.get(id) ?? {
|
| 417 |
-
name: result.model_info.name,
|
| 418 |
-
developer: result.model_info.developer ?? "",
|
| 419 |
-
scores: new Map<string, number | null>(),
|
| 420 |
-
}
|
| 421 |
-
existing.scores.set(sub.evaluation_name, result.score)
|
| 422 |
-
modelScores.set(id, existing)
|
| 423 |
-
}
|
| 424 |
-
}
|
| 425 |
-
|
| 426 |
-
const modelList = Array.from(modelScores.entries())
|
| 427 |
-
.map(([id, data]) => {
|
| 428 |
-
const validScores = Array.from(data.scores.values()).filter(
|
| 429 |
-
(s): s is number => s != null && Number.isFinite(s) && s > -99
|
| 430 |
-
)
|
| 431 |
-
const avg = validScores.length > 0
|
| 432 |
-
? validScores.reduce((a, b) => a + b, 0) / validScores.length
|
| 433 |
-
: 0
|
| 434 |
-
// Parse model size from name (e.g., "70B", "8b", "1.5B", "405b")
|
| 435 |
-
let sizeB: number | null = null
|
| 436 |
-
const sizeMatch = (data.name + " " + id).match(/\b(\d+(?:\.\d+)?)\s*[bB]\b/)
|
| 437 |
-
if (sizeMatch) sizeB = parseFloat(sizeMatch[1])
|
| 438 |
-
|
| 439 |
-
return { id, name: data.name, developer: data.developer, avg, scores: data.scores, sizeB }
|
| 440 |
-
})
|
| 441 |
-
|
| 442 |
-
return { models: modelList, metrics: metricNames }
|
| 443 |
-
}, [subSummaries])
|
| 444 |
-
|
| 445 |
-
const visibleMetrics = useMemo(
|
| 446 |
-
() => metrics.filter((m) => !hiddenCols.has(m)),
|
| 447 |
-
[metrics, hiddenCols]
|
| 448 |
-
)
|
| 449 |
-
|
| 450 |
-
const sortedModels = useMemo(() => {
|
| 451 |
-
return [...models].sort((a, b) => {
|
| 452 |
-
if (sortCol === "name") {
|
| 453 |
-
return sortAsc ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name)
|
| 454 |
-
}
|
| 455 |
-
const aVal = sortCol === "avg" ? a.avg : (a.scores.get(sortCol) ?? -Infinity)
|
| 456 |
-
const bVal = sortCol === "avg" ? b.avg : (b.scores.get(sortCol) ?? -Infinity)
|
| 457 |
-
return sortAsc ? aVal - bVal : bVal - aVal
|
| 458 |
-
})
|
| 459 |
-
}, [models, sortCol, sortAsc])
|
| 460 |
-
|
| 461 |
-
const maxStepIndex = PARAM_RANGE_VALUES.length - 1
|
| 462 |
-
const numericMinParams = minParamStep <= 0 ? null : (PARAM_RANGE_VALUES[minParamStep] ?? null)
|
| 463 |
-
const numericMaxParams = maxParamStep >= maxStepIndex ? null : (PARAM_RANGE_VALUES[maxParamStep] ?? null)
|
| 464 |
-
|
| 465 |
-
const query = search.trim().toLowerCase()
|
| 466 |
-
const filteredModels = sortedModels.filter((m) => {
|
| 467 |
-
if (query && !(
|
| 468 |
-
m.name.toLowerCase().includes(query) ||
|
| 469 |
-
m.developer.toLowerCase().includes(query) ||
|
| 470 |
-
m.id.toLowerCase().includes(query)
|
| 471 |
-
)) return false
|
| 472 |
-
if (numericMinParams != null && (m.sizeB == null || m.sizeB < numericMinParams)) return false
|
| 473 |
-
if (numericMaxParams != null && (m.sizeB == null || m.sizeB > numericMaxParams)) return false
|
| 474 |
-
return true
|
| 475 |
-
})
|
| 476 |
-
|
| 477 |
-
const pagedModels = filteredModels.slice(0, page * PAGE_SIZE)
|
| 478 |
-
const hasMore = pagedModels.length < filteredModels.length
|
| 479 |
-
|
| 480 |
-
// Color coding per column
|
| 481 |
-
const metricRanges = useMemo(() => {
|
| 482 |
-
const ranges = new Map<string, { min: number; max: number }>()
|
| 483 |
-
for (const metric of visibleMetrics) {
|
| 484 |
-
const scores = models.map((m) => m.scores.get(metric)).filter(
|
| 485 |
-
(s): s is number => s != null && Number.isFinite(s) && s > -99
|
| 486 |
-
)
|
| 487 |
-
if (scores.length > 0) {
|
| 488 |
-
ranges.set(metric, { min: Math.min(...scores), max: Math.max(...scores) })
|
| 489 |
-
}
|
| 490 |
-
}
|
| 491 |
-
return ranges
|
| 492 |
-
}, [models, visibleMetrics])
|
| 493 |
-
|
| 494 |
-
function isValidScore(score: number | null | undefined): score is number {
|
| 495 |
-
return score != null && Number.isFinite(score) && score > -99
|
| 496 |
-
}
|
| 497 |
-
|
| 498 |
-
function scoreColor(metric: string, score: number): string {
|
| 499 |
-
const range = metricRanges.get(metric)
|
| 500 |
-
if (!range || range.max === range.min) return ""
|
| 501 |
-
const lower = metricDirection.get(metric) ?? false
|
| 502 |
-
const pct = lower
|
| 503 |
-
? (range.max - score) / (range.max - range.min)
|
| 504 |
-
: (score - range.min) / (range.max - range.min)
|
| 505 |
-
if (pct >= 0.8) return "bg-emerald-100 dark:bg-emerald-950/40 text-emerald-800 dark:text-emerald-200"
|
| 506 |
-
if (pct >= 0.6) return "bg-sky-50 dark:bg-sky-950/30 text-sky-800 dark:text-sky-200"
|
| 507 |
-
if (pct <= 0.2) return "bg-red-50 dark:bg-red-950/30 text-red-800 dark:text-red-200"
|
| 508 |
-
return ""
|
| 509 |
-
}
|
| 510 |
-
|
| 511 |
-
function formatScore(score: number): string {
|
| 512 |
-
if (Math.abs(score) >= 100) return score.toFixed(1)
|
| 513 |
-
if (Math.abs(score) >= 10) return score.toFixed(2)
|
| 514 |
-
return score.toFixed(3).replace(/0+$/g, "").replace(/\.$/, "")
|
| 515 |
-
}
|
| 516 |
-
|
| 517 |
-
function handleSort(col: string) {
|
| 518 |
-
if (sortCol === col) setSortAsc(!sortAsc)
|
| 519 |
-
else { setSortCol(col); setSortAsc(false) }
|
| 520 |
-
}
|
| 521 |
-
|
| 522 |
-
const sortIndicator = (col: string) =>
|
| 523 |
-
sortCol === col ? (sortAsc ? " ▲" : " ▼") : ""
|
| 524 |
-
|
| 525 |
-
function toggleCol(metric: string) {
|
| 526 |
-
setHiddenCols((prev) => {
|
| 527 |
-
const next = new Set(prev)
|
| 528 |
-
if (next.has(metric)) next.delete(metric)
|
| 529 |
-
else next.add(metric)
|
| 530 |
-
return next
|
| 531 |
-
})
|
| 532 |
-
}
|
| 533 |
-
|
| 534 |
-
if (subSummaries.length === 0) {
|
| 535 |
-
return (
|
| 536 |
-
<div className="py-12 text-center text-muted-foreground">
|
| 537 |
-
Loading sub-benchmark data for the matrix view...
|
| 538 |
-
</div>
|
| 539 |
-
)
|
| 540 |
-
}
|
| 541 |
-
|
| 542 |
-
return (
|
| 543 |
-
<div className="space-y-4">
|
| 544 |
-
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
|
| 545 |
-
<div className="relative w-full max-w-sm">
|
| 546 |
-
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
| 547 |
-
<Input
|
| 548 |
-
value={search}
|
| 549 |
-
onChange={(e) => onSearchChange(e.target.value)}
|
| 550 |
-
placeholder="Search models..."
|
| 551 |
-
className="pl-9"
|
| 552 |
-
/>
|
| 553 |
-
</div>
|
| 554 |
-
<div className="rounded-xl border border-border/70 bg-muted/15 px-4 py-2">
|
| 555 |
-
<div className="flex items-center gap-3">
|
| 556 |
-
<span className="shrink-0 text-sm font-medium text-foreground">Parameters</span>
|
| 557 |
-
|
| 558 |
-
<div className="min-w-0 flex-1 w-[min(92vw,300px)]">
|
| 559 |
-
<div className="relative mb-1 h-4 text-[11px] text-muted-foreground">
|
| 560 |
-
{PARAM_RANGE_MARKERS.map((marker) => (
|
| 561 |
-
<span
|
| 562 |
-
key={marker.label}
|
| 563 |
-
className="absolute top-0 whitespace-nowrap"
|
| 564 |
-
style={{
|
| 565 |
-
left: `${(marker.step / maxStepIndex) * 100}%`,
|
| 566 |
-
transform:
|
| 567 |
-
marker.step === 0 ? "translateX(0)"
|
| 568 |
-
: marker.step === maxStepIndex ? "translateX(-100%)"
|
| 569 |
-
: "translateX(-50%)",
|
| 570 |
-
}}
|
| 571 |
-
>
|
| 572 |
-
{marker.label}
|
| 573 |
-
</span>
|
| 574 |
-
))}
|
| 575 |
-
</div>
|
| 576 |
-
|
| 577 |
-
<div className="relative h-4">
|
| 578 |
-
<div className="absolute inset-x-1.5 top-1/2 h-[3px] -translate-y-1/2 rounded-full bg-border/80" />
|
| 579 |
-
<div className="absolute inset-x-1.5 top-1/2 h-[3px] -translate-y-1/2">
|
| 580 |
-
<div
|
| 581 |
-
className="absolute inset-y-0 rounded-full bg-foreground transition-[left,right] duration-300 ease-[var(--ease-out-quint)]"
|
| 582 |
-
style={{
|
| 583 |
-
left: `${(minParamStep / maxStepIndex) * 100}%`,
|
| 584 |
-
right: `${Math.max(100 - (maxParamStep / maxStepIndex) * 100, 0)}%`,
|
| 585 |
-
}}
|
| 586 |
-
/>
|
| 587 |
-
</div>
|
| 588 |
-
|
| 589 |
-
<div className="absolute inset-x-1.5 top-1/2 -translate-y-1/2">
|
| 590 |
-
{PARAM_RANGE_VALUES.map((_, stepIndex) => (
|
| 591 |
-
<span
|
| 592 |
-
key={`param-tick-${stepIndex}`}
|
| 593 |
-
className="absolute top-0 h-2 w-px -translate-x-1/2 rounded-full bg-border"
|
| 594 |
-
style={{ left: `${(stepIndex / maxStepIndex) * 100}%` }}
|
| 595 |
-
aria-hidden="true"
|
| 596 |
-
/>
|
| 597 |
-
))}
|
| 598 |
-
</div>
|
| 599 |
-
|
| 600 |
-
<input
|
| 601 |
-
type="range"
|
| 602 |
-
min={0}
|
| 603 |
-
max={maxStepIndex}
|
| 604 |
-
step={1}
|
| 605 |
-
value={minParamStep}
|
| 606 |
-
onChange={(e) => {
|
| 607 |
-
const v = Number(e.target.value)
|
| 608 |
-
setMinParamStep(Math.min(v, maxParamStep))
|
| 609 |
-
}}
|
| 610 |
-
className="param-range-input"
|
| 611 |
-
aria-label="Minimum parameter filter"
|
| 612 |
-
/>
|
| 613 |
-
<input
|
| 614 |
-
type="range"
|
| 615 |
-
min={0}
|
| 616 |
-
max={maxStepIndex}
|
| 617 |
-
step={1}
|
| 618 |
-
value={maxParamStep}
|
| 619 |
-
onChange={(e) => {
|
| 620 |
-
const v = Number(e.target.value)
|
| 621 |
-
setMaxParamStep(Math.max(v, minParamStep))
|
| 622 |
-
}}
|
| 623 |
-
className="param-range-input"
|
| 624 |
-
aria-label="Maximum parameter filter"
|
| 625 |
-
/>
|
| 626 |
-
</div>
|
| 627 |
-
</div>
|
| 628 |
-
|
| 629 |
-
<span className="shrink-0 text-[11px] text-muted-foreground">
|
| 630 |
-
{formatParamBoundLabel(minParamStep, "min")} to {formatParamBoundLabel(maxParamStep, "max")}
|
| 631 |
-
</span>
|
| 632 |
-
</div>
|
| 633 |
-
</div>
|
| 634 |
-
<div className="text-sm text-muted-foreground whitespace-nowrap">
|
| 635 |
-
{filteredModels.length} models × {visibleMetrics.length} metrics
|
| 636 |
-
</div>
|
| 637 |
-
</div>
|
| 638 |
-
|
| 639 |
-
{/* Column toggles */}
|
| 640 |
-
<div className="flex flex-wrap gap-1.5">
|
| 641 |
-
{metrics.map((metric) => (
|
| 642 |
-
<button
|
| 643 |
-
key={metric}
|
| 644 |
-
type="button"
|
| 645 |
-
onClick={() => toggleCol(metric)}
|
| 646 |
-
className={`rounded-full border px-2.5 py-1 text-[11px] font-medium transition-colors ${
|
| 647 |
-
hiddenCols.has(metric)
|
| 648 |
-
? "border-border/40 bg-muted/20 text-muted-foreground/50 line-through"
|
| 649 |
-
: "border-border/70 bg-background text-foreground hover:border-primary/50"
|
| 650 |
-
}`}
|
| 651 |
-
>
|
| 652 |
-
{metric}
|
| 653 |
-
</button>
|
| 654 |
-
))}
|
| 655 |
-
</div>
|
| 656 |
-
|
| 657 |
-
<div className="overflow-x-auto rounded-lg border">
|
| 658 |
-
<table className="w-full text-sm" style={{ tableLayout: "fixed" }}>
|
| 659 |
-
<colgroup>
|
| 660 |
-
<col style={{ width: 48 }} />
|
| 661 |
-
<col style={{ width: 220 }} />
|
| 662 |
-
<col style={{ width: 90 }} />
|
| 663 |
-
{visibleMetrics.map((m) => (
|
| 664 |
-
<col key={m} style={{ width: 110 }} />
|
| 665 |
-
))}
|
| 666 |
-
</colgroup>
|
| 667 |
-
<thead>
|
| 668 |
-
<tr className="border-b bg-muted/30">
|
| 669 |
-
<th className="sticky left-0 z-10 bg-muted/30 px-3 py-2 text-left font-semibold">#</th>
|
| 670 |
-
<th
|
| 671 |
-
className="sticky left-[48px] z-10 bg-muted/30 px-3 py-2 text-left font-semibold cursor-pointer select-none hover:text-primary"
|
| 672 |
-
onClick={() => handleSort("name")}
|
| 673 |
-
>
|
| 674 |
-
Model{sortIndicator("name")}
|
| 675 |
-
</th>
|
| 676 |
-
<th
|
| 677 |
-
className="px-3 py-2 text-right font-semibold cursor-pointer select-none hover:text-primary"
|
| 678 |
-
onClick={() => handleSort("avg")}
|
| 679 |
-
>
|
| 680 |
-
Avg{sortIndicator("avg")}
|
| 681 |
-
</th>
|
| 682 |
-
{visibleMetrics.map((metric) => (
|
| 683 |
-
<th
|
| 684 |
-
key={metric}
|
| 685 |
-
className="px-3 py-2 text-right font-semibold cursor-pointer select-none hover:text-primary truncate"
|
| 686 |
-
onClick={() => handleSort(metric)}
|
| 687 |
-
title={metric}
|
| 688 |
-
>
|
| 689 |
-
{metric}{sortIndicator(metric)}
|
| 690 |
-
</th>
|
| 691 |
-
))}
|
| 692 |
-
</tr>
|
| 693 |
-
</thead>
|
| 694 |
-
<tbody>
|
| 695 |
-
{pagedModels.map((model, idx) => (
|
| 696 |
-
<tr key={model.id} className="border-b hover:bg-muted/20 transition-colors">
|
| 697 |
-
<td className="sticky left-0 z-10 bg-background px-3 py-2 text-muted-foreground tabular-nums">
|
| 698 |
-
{idx + 1}
|
| 699 |
-
</td>
|
| 700 |
-
<td className="sticky left-[48px] z-10 bg-background px-3 py-2">
|
| 701 |
-
<div className="font-medium truncate">{model.name}</div>
|
| 702 |
-
<div className="text-xs text-muted-foreground truncate">{model.developer}</div>
|
| 703 |
-
</td>
|
| 704 |
-
<td className="px-3 py-2 text-right font-semibold tabular-nums">
|
| 705 |
-
{formatScore(model.avg)}
|
| 706 |
-
</td>
|
| 707 |
-
{visibleMetrics.map((metric) => {
|
| 708 |
-
const score = model.scores.get(metric)
|
| 709 |
-
const valid = isValidScore(score)
|
| 710 |
-
return (
|
| 711 |
-
<td
|
| 712 |
-
key={metric}
|
| 713 |
-
className={`px-3 py-2 text-right tabular-nums ${valid ? scoreColor(metric, score) : "text-muted-foreground"}`}
|
| 714 |
-
>
|
| 715 |
-
{valid ? formatScore(score) : "—"}
|
| 716 |
-
</td>
|
| 717 |
-
)
|
| 718 |
-
})}
|
| 719 |
-
</tr>
|
| 720 |
-
))}
|
| 721 |
-
</tbody>
|
| 722 |
-
</table>
|
| 723 |
-
</div>
|
| 724 |
-
|
| 725 |
-
{hasMore && (
|
| 726 |
-
<div className="text-center">
|
| 727 |
-
<Button variant="outline" onClick={() => setPage((p) => p + 1)}>
|
| 728 |
-
Load more ({filteredModels.length - pagedModels.length} remaining)
|
| 729 |
-
</Button>
|
| 730 |
-
</div>
|
| 731 |
-
)}
|
| 732 |
-
</div>
|
| 733 |
-
)
|
| 734 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/models/[...id]/page (1).tsx
DELETED
|
@@ -1,355 +0,0 @@
|
|
| 1 |
-
"use client"
|
| 2 |
-
|
| 3 |
-
import { startTransition, useCallback, useEffect, useMemo, useState } from "react"
|
| 4 |
-
import { useParams, useRouter, useSearchParams } from "next/navigation"
|
| 5 |
-
import { Button } from "@/components/ui/button"
|
| 6 |
-
import { ArrowLeft } from "lucide-react"
|
| 7 |
-
import { Navigation } from "@/components/navigation"
|
| 8 |
-
import { BenchmarkDetail } from "@/components/benchmark-detail"
|
| 9 |
-
import type { BenchmarkCard, ModelEvaluationSummary } from "@/lib/eval-processing"
|
| 10 |
-
import {
|
| 11 |
-
fetchBenchmarkMetadata,
|
| 12 |
-
fetchComparisonIndex,
|
| 13 |
-
fetchEvalHierarchy,
|
| 14 |
-
fetchModelSummary,
|
| 15 |
-
fetchModelCards,
|
| 16 |
-
} from "@/lib/dashboard-data-client"
|
| 17 |
-
import type { BenchmarkEvaluationCardData } from "@/components/benchmark-evaluation-card"
|
| 18 |
-
import type { ComparisonIndex, EvalHierarchy } from "@/lib/backend-artifacts"
|
| 19 |
-
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
| 20 |
-
|
| 21 |
-
export default function ModelDetailPage() {
|
| 22 |
-
const params = useParams()
|
| 23 |
-
const router = useRouter()
|
| 24 |
-
const searchParams = useSearchParams()
|
| 25 |
-
const [summary, setSummary] = useState<ModelEvaluationSummary | null>(null)
|
| 26 |
-
const [benchmarkCards, setBenchmarkCards] = useState<Record<string, BenchmarkCard>>({})
|
| 27 |
-
const [modelCards, setModelCards] = useState<BenchmarkEvaluationCardData[]>([])
|
| 28 |
-
const [evalHierarchy, setEvalHierarchy] = useState<EvalHierarchy | null>(null)
|
| 29 |
-
const [comparisonIndex, setComparisonIndex] = useState<ComparisonIndex | null>(null)
|
| 30 |
-
const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null)
|
| 31 |
-
const [loading, setLoading] = useState(true)
|
| 32 |
-
const [error, setError] = useState<string | null>(null)
|
| 33 |
-
const routeId = params.id as string
|
| 34 |
-
|
| 35 |
-
const getVariantFromQuery = useCallback(
|
| 36 |
-
(modelSummary: ModelEvaluationSummary) => {
|
| 37 |
-
const requestedVersion = searchParams.get("version")
|
| 38 |
-
|
| 39 |
-
if (!requestedVersion || modelSummary.variants.length === 0) {
|
| 40 |
-
return modelSummary.variants[0] ?? null
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
return (
|
| 44 |
-
modelSummary.variants.find(
|
| 45 |
-
(variant) =>
|
| 46 |
-
variant.variant_key === requestedVersion ||
|
| 47 |
-
variant.variant_id === requestedVersion
|
| 48 |
-
) ?? modelSummary.variants[0] ?? null
|
| 49 |
-
)
|
| 50 |
-
},
|
| 51 |
-
[searchParams]
|
| 52 |
-
)
|
| 53 |
-
|
| 54 |
-
const handleBack = useCallback(() => {
|
| 55 |
-
if (typeof window !== "undefined") {
|
| 56 |
-
const referrer = document.referrer
|
| 57 |
-
|
| 58 |
-
if (referrer) {
|
| 59 |
-
try {
|
| 60 |
-
const referrerUrl = new URL(referrer)
|
| 61 |
-
if (referrerUrl.origin === window.location.origin) {
|
| 62 |
-
router.back()
|
| 63 |
-
return
|
| 64 |
-
}
|
| 65 |
-
} catch {
|
| 66 |
-
// Fall through to a deterministic in-app destination.
|
| 67 |
-
}
|
| 68 |
-
}
|
| 69 |
-
}
|
| 70 |
-
|
| 71 |
-
router.push("/models")
|
| 72 |
-
}, [router])
|
| 73 |
-
|
| 74 |
-
const handleVariantChange = useCallback(
|
| 75 |
-
(nextVariantId: string) => {
|
| 76 |
-
setSelectedVariantId(nextVariantId)
|
| 77 |
-
|
| 78 |
-
if (!summary || summary.variants.length <= 1 || !routeId) {
|
| 79 |
-
return
|
| 80 |
-
}
|
| 81 |
-
|
| 82 |
-
const nextVariant = summary.variants.find((variant) => variant.variant_id === nextVariantId)
|
| 83 |
-
if (!nextVariant) {
|
| 84 |
-
return
|
| 85 |
-
}
|
| 86 |
-
|
| 87 |
-
const nextParams = new URLSearchParams(searchParams.toString())
|
| 88 |
-
const currentVersion = nextParams.get("version")
|
| 89 |
-
const nextVersion = nextVariant.variant_key
|
| 90 |
-
|
| 91 |
-
if (currentVersion === nextVersion) {
|
| 92 |
-
return
|
| 93 |
-
}
|
| 94 |
-
|
| 95 |
-
nextParams.set("version", nextVersion)
|
| 96 |
-
const nextQuery = nextParams.toString()
|
| 97 |
-
router.replace(
|
| 98 |
-
nextQuery ? `/models/${routeId}?${nextQuery}` : `/models/${routeId}`,
|
| 99 |
-
{ scroll: false }
|
| 100 |
-
)
|
| 101 |
-
},
|
| 102 |
-
[routeId, router, searchParams, summary]
|
| 103 |
-
)
|
| 104 |
-
|
| 105 |
-
useEffect(() => {
|
| 106 |
-
let isCancelled = false
|
| 107 |
-
|
| 108 |
-
const loadCoreData = async () => {
|
| 109 |
-
try {
|
| 110 |
-
const [modelSummary, cards, hierarchy] = await Promise.all([
|
| 111 |
-
fetchModelSummary(routeId),
|
| 112 |
-
fetchBenchmarkMetadata(),
|
| 113 |
-
fetchEvalHierarchy().catch((err) => {
|
| 114 |
-
console.warn("Failed to load eval-hierarchy:", err)
|
| 115 |
-
return null as EvalHierarchy | null
|
| 116 |
-
}),
|
| 117 |
-
])
|
| 118 |
-
if (isCancelled) {
|
| 119 |
-
return
|
| 120 |
-
}
|
| 121 |
-
|
| 122 |
-
setSummary(modelSummary)
|
| 123 |
-
setBenchmarkCards(cards)
|
| 124 |
-
setEvalHierarchy(hierarchy)
|
| 125 |
-
setSelectedVariantId((current) => current ?? modelSummary.variants[0]?.variant_id ?? null)
|
| 126 |
-
} catch (err) {
|
| 127 |
-
if (isCancelled) {
|
| 128 |
-
return
|
| 129 |
-
}
|
| 130 |
-
|
| 131 |
-
console.error("Failed to load model:", err)
|
| 132 |
-
setError("Failed to load model data")
|
| 133 |
-
} finally {
|
| 134 |
-
if (!isCancelled) {
|
| 135 |
-
setLoading(false)
|
| 136 |
-
}
|
| 137 |
-
}
|
| 138 |
-
}
|
| 139 |
-
|
| 140 |
-
loadCoreData()
|
| 141 |
-
|
| 142 |
-
return () => {
|
| 143 |
-
isCancelled = true
|
| 144 |
-
}
|
| 145 |
-
}, [routeId])
|
| 146 |
-
|
| 147 |
-
useEffect(() => {
|
| 148 |
-
let isCancelled = false
|
| 149 |
-
|
| 150 |
-
const loadAuxiliaryData = async () => {
|
| 151 |
-
const [allModelCards, compIndex] = await Promise.all([
|
| 152 |
-
fetchModelCards().catch((err) => {
|
| 153 |
-
console.warn("Failed to load model cards:", err)
|
| 154 |
-
return [] as BenchmarkEvaluationCardData[]
|
| 155 |
-
}),
|
| 156 |
-
fetchComparisonIndex().catch((err) => {
|
| 157 |
-
console.warn("Failed to load comparison-index:", err)
|
| 158 |
-
return null as ComparisonIndex | null
|
| 159 |
-
}),
|
| 160 |
-
])
|
| 161 |
-
|
| 162 |
-
if (isCancelled) {
|
| 163 |
-
return
|
| 164 |
-
}
|
| 165 |
-
|
| 166 |
-
startTransition(() => {
|
| 167 |
-
setModelCards(allModelCards)
|
| 168 |
-
setComparisonIndex(compIndex)
|
| 169 |
-
})
|
| 170 |
-
}
|
| 171 |
-
|
| 172 |
-
loadAuxiliaryData()
|
| 173 |
-
|
| 174 |
-
return () => {
|
| 175 |
-
isCancelled = true
|
| 176 |
-
}
|
| 177 |
-
}, [routeId])
|
| 178 |
-
|
| 179 |
-
useEffect(() => {
|
| 180 |
-
if (!summary?.variants.length) {
|
| 181 |
-
return
|
| 182 |
-
}
|
| 183 |
-
|
| 184 |
-
const requestedVariant = getVariantFromQuery(summary)
|
| 185 |
-
if (requestedVariant && requestedVariant.variant_id !== selectedVariantId) {
|
| 186 |
-
setSelectedVariantId(requestedVariant.variant_id)
|
| 187 |
-
}
|
| 188 |
-
}, [getVariantFromQuery, searchParams, summary])
|
| 189 |
-
|
| 190 |
-
useEffect(() => {
|
| 191 |
-
if (!summary?.variants.length || !routeId) {
|
| 192 |
-
return
|
| 193 |
-
}
|
| 194 |
-
|
| 195 |
-
const requestedVersion = searchParams.get("version")
|
| 196 |
-
if (!requestedVersion) {
|
| 197 |
-
return
|
| 198 |
-
}
|
| 199 |
-
|
| 200 |
-
const matchesKnownVariant = summary.variants.some(
|
| 201 |
-
(variant) =>
|
| 202 |
-
variant.variant_key === requestedVersion ||
|
| 203 |
-
variant.variant_id === requestedVersion
|
| 204 |
-
)
|
| 205 |
-
|
| 206 |
-
if (matchesKnownVariant) {
|
| 207 |
-
return
|
| 208 |
-
}
|
| 209 |
-
|
| 210 |
-
const nextParams = new URLSearchParams(searchParams.toString())
|
| 211 |
-
nextParams.delete("version")
|
| 212 |
-
const nextQuery = nextParams.toString()
|
| 213 |
-
|
| 214 |
-
router.replace(
|
| 215 |
-
nextQuery ? `/models/${routeId}?${nextQuery}` : `/models/${routeId}`,
|
| 216 |
-
{ scroll: false }
|
| 217 |
-
)
|
| 218 |
-
}, [routeId, router, searchParams, summary])
|
| 219 |
-
|
| 220 |
-
const selectedVariant = useMemo(() => {
|
| 221 |
-
if (!summary) {
|
| 222 |
-
return null
|
| 223 |
-
}
|
| 224 |
-
|
| 225 |
-
if (!summary.variants.length) {
|
| 226 |
-
return summary
|
| 227 |
-
}
|
| 228 |
-
|
| 229 |
-
return (
|
| 230 |
-
summary.variants.find((variant) => variant.variant_id === selectedVariantId) ??
|
| 231 |
-
summary.variants[0]
|
| 232 |
-
)
|
| 233 |
-
}, [selectedVariantId, summary])
|
| 234 |
-
|
| 235 |
-
useEffect(() => {
|
| 236 |
-
if (!summary) {
|
| 237 |
-
return
|
| 238 |
-
}
|
| 239 |
-
|
| 240 |
-
const titleParts = [summary.model_family_name]
|
| 241 |
-
if (selectedVariant && "variant_key" in selectedVariant && selectedVariant.variant_key !== "base") {
|
| 242 |
-
titleParts.push(selectedVariant.variant_label)
|
| 243 |
-
}
|
| 244 |
-
|
| 245 |
-
document.title = `${titleParts.join(" · ")} - AI Evaluation Dashboard`
|
| 246 |
-
}, [selectedVariant, summary])
|
| 247 |
-
|
| 248 |
-
if (loading) {
|
| 249 |
-
return (
|
| 250 |
-
<div className="min-h-screen bg-background">
|
| 251 |
-
<Navigation />
|
| 252 |
-
<main className="container mx-auto px-4 py-8">
|
| 253 |
-
<div className="flex items-center justify-center h-96">
|
| 254 |
-
<div className="text-lg text-muted-foreground">Loading model details...</div>
|
| 255 |
-
</div>
|
| 256 |
-
</main>
|
| 257 |
-
</div>
|
| 258 |
-
)
|
| 259 |
-
}
|
| 260 |
-
|
| 261 |
-
if (error || !summary) {
|
| 262 |
-
return (
|
| 263 |
-
<div className="min-h-screen bg-background">
|
| 264 |
-
<Navigation />
|
| 265 |
-
<main className="container mx-auto px-4 py-8">
|
| 266 |
-
<div className="flex flex-col items-center justify-center h-96 space-y-4">
|
| 267 |
-
<div className="text-lg text-muted-foreground">{error || "Model not found"}</div>
|
| 268 |
-
<Button onClick={handleBack}>
|
| 269 |
-
<ArrowLeft className="mr-2 h-4 w-4" />
|
| 270 |
-
Back
|
| 271 |
-
</Button>
|
| 272 |
-
</div>
|
| 273 |
-
</main>
|
| 274 |
-
</div>
|
| 275 |
-
)
|
| 276 |
-
}
|
| 277 |
-
|
| 278 |
-
const detailSummary = selectedVariant ?? summary
|
| 279 |
-
const hasVariantTabs = summary.variants.length > 1
|
| 280 |
-
|
| 281 |
-
return (
|
| 282 |
-
<div className="min-h-screen bg-background">
|
| 283 |
-
<Navigation />
|
| 284 |
-
<div className="border-b bg-muted/30">
|
| 285 |
-
<div className="container mx-auto px-4 sm:px-6 py-4 sm:py-6">
|
| 286 |
-
<div className="flex items-center gap-3 sm:hidden">
|
| 287 |
-
<Button
|
| 288 |
-
variant="ghost"
|
| 289 |
-
size="sm"
|
| 290 |
-
onClick={handleBack}
|
| 291 |
-
className="shrink-0"
|
| 292 |
-
>
|
| 293 |
-
<ArrowLeft className="h-4 w-4" />
|
| 294 |
-
</Button>
|
| 295 |
-
<div className="flex-1 text-center">
|
| 296 |
-
<h2 className="text-base font-medium tracking-tight text-foreground/90 sm:text-lg">
|
| 297 |
-
Model details
|
| 298 |
-
</h2>
|
| 299 |
-
</div>
|
| 300 |
-
</div>
|
| 301 |
-
|
| 302 |
-
<div className="hidden sm:grid sm:grid-cols-[auto_1fr_auto] sm:items-center sm:gap-4">
|
| 303 |
-
<Button
|
| 304 |
-
variant="ghost"
|
| 305 |
-
onClick={handleBack}
|
| 306 |
-
>
|
| 307 |
-
<ArrowLeft className="mr-2 h-4 w-4" />
|
| 308 |
-
Back
|
| 309 |
-
</Button>
|
| 310 |
-
<div className="text-center">
|
| 311 |
-
<h2 className="text-xl font-medium tracking-tight text-foreground/90 md:text-2xl">
|
| 312 |
-
Model details
|
| 313 |
-
</h2>
|
| 314 |
-
</div>
|
| 315 |
-
<div />
|
| 316 |
-
</div>
|
| 317 |
-
|
| 318 |
-
{hasVariantTabs ? (
|
| 319 |
-
<div className="mt-4 rounded-2xl border border-border/70 bg-background/80 px-3 py-3 shadow-sm">
|
| 320 |
-
<div className="mb-2 text-[11px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
|
| 321 |
-
Versions
|
| 322 |
-
</div>
|
| 323 |
-
<Tabs
|
| 324 |
-
value={selectedVariantId ?? summary.variants[0].variant_id}
|
| 325 |
-
onValueChange={handleVariantChange}
|
| 326 |
-
className="gap-0"
|
| 327 |
-
>
|
| 328 |
-
<TabsList className="flex w-full flex-wrap gap-2">
|
| 329 |
-
{summary.variants.map((variant) => (
|
| 330 |
-
<TabsTrigger
|
| 331 |
-
key={variant.variant_id}
|
| 332 |
-
value={variant.variant_id}
|
| 333 |
-
className="h-auto rounded-full border border-border/80 bg-muted/30 px-3 py-1.5 text-xs sm:text-sm data-[state=active]:border-foreground/20 data-[state=active]:bg-background"
|
| 334 |
-
>
|
| 335 |
-
{variant.variant_label}
|
| 336 |
-
</TabsTrigger>
|
| 337 |
-
))}
|
| 338 |
-
</TabsList>
|
| 339 |
-
</Tabs>
|
| 340 |
-
</div>
|
| 341 |
-
) : null}
|
| 342 |
-
</div>
|
| 343 |
-
</div>
|
| 344 |
-
<main className="container mx-auto px-4 py-8">
|
| 345 |
-
<BenchmarkDetail
|
| 346 |
-
summary={detailSummary}
|
| 347 |
-
benchmarkCards={benchmarkCards}
|
| 348 |
-
modelCards={modelCards}
|
| 349 |
-
evalHierarchy={evalHierarchy}
|
| 350 |
-
comparisonIndex={comparisonIndex}
|
| 351 |
-
/>
|
| 352 |
-
</main>
|
| 353 |
-
</div>
|
| 354 |
-
)
|
| 355 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
output/hierarchy_explorer.html
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
public/peer-ranks.json
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|