evijit HF Staff Claude Opus 4.7 (1M context) commited on
Commit
445ce35
·
1 Parent(s): b6dab21

Untrack accidentally-committed noise files; gitignore the patterns

Browse files

The 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 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