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

view-data: paranoid CAST every output column to a primitive type

Browse files

The previous patch left simple scalars (VARCHAR / BOOLEAN / DOUBLE /
INTEGER) untouched, on the assumption the binding handles those
natively. The deployed log still surfaces "Invalid Error: don't know
what type:" on the new explicit-column SELECT, which means whatever
the binding is choking on is reachable even without r.* expansion
and even with every nested type wrapped. The only thing left to vary
is whether the binding ever sees the parquet's native logical types
at all.

Force every output column through `CAST(... AS <primitive>)`. The
binding now only ever sees VARCHAR / BOOLEAN / INTEGER / DOUBLE in
the result schema regardless of what type the underlying parquet
column declared. Local prod build keeps returning 200 on
/api/model-summary with model_info correctly nested.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

app/developers/[...id]/page (1).tsx ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,734 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,355 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }
lib/view-data.ts CHANGED
@@ -97,56 +97,56 @@ const EVAL_LIST_COLUMNS = `
97
  // the binding only ever sees primitive types; parseMaybeJson restores
98
  // the shape on the JS side.
99
  const CELL_JOIN_COLUMNS = `
100
- r.evaluation_id,
101
- r.metric_summary_id,
102
- r.composite_slug,
103
- r.composite_display_name,
104
- r.benchmark_id,
105
- r.family_id,
106
- r.family_display_name,
107
- r.is_slice,
108
- r.parent_benchmark_id,
109
- r.metric_id,
110
- r.model_key,
111
- r.model_id,
112
- r.model_route_id,
113
  CAST(to_json(r.model_info) AS VARCHAR) AS model_info,
114
  CAST(to_json(r.generation_config) AS VARCHAR) AS generation_config,
115
- r.metric_display_name,
116
- r.metric_unit,
117
- r.lower_is_better,
118
- r.category,
119
- r.score,
120
  CAST(to_json(r.score_details) AS VARCHAR) AS score_details,
121
- r.position,
122
  CAST(r.evaluation_timestamp AS VARCHAR) AS evaluation_timestamp,
123
  CAST(to_json(r.source_metadata) AS VARCHAR) AS source_metadata,
124
  CAST(to_json(r.source_data) AS VARCHAR) AS source_data,
125
- r.source_record_url,
126
- r.is_summary_score,
127
  CAST(to_json(r.aggregate_components) AS VARCHAR) AS aggregate_components,
128
  CAST(to_json(r.evalcards_annotations) AS VARCHAR) AS evalcards_annotations,
129
  CAST(to_json(r.eval_library) AS VARCHAR) AS eval_library,
130
- r.instance_file_path,
131
- e.evaluation_name AS eval_evaluation_name,
132
- e.canonical_display_name AS eval_canonical_display_name,
133
- e.benchmark_id AS eval_benchmark_id,
134
- e.composite_slug AS eval_composite_slug,
135
- e.composite_display_name AS eval_composite_display_name,
136
- e.family_id AS eval_family_id,
137
- e.family_display_name AS eval_family_display_name,
138
- e.is_slice AS eval_is_slice,
139
- e.parent_benchmark_id AS eval_parent_benchmark_id,
140
- e.composite_slug AS eval_composite_benchmark_key,
141
- e.composite_display_name AS eval_composite_benchmark_name,
142
- e.family_display_name AS eval_benchmark_family_name,
143
- e.category AS eval_category,
144
  CAST(to_json(e.metric_config) AS VARCHAR) AS eval_metric_config,
145
  CAST(to_json(e.source_data) AS VARCHAR) AS eval_source_data,
146
  CAST(to_json(e.benchmark_card) AS VARCHAR) AS eval_benchmark_card,
147
  CAST(to_json(e.tags) AS VARCHAR) AS eval_tags,
148
- e.is_summary_score AS eval_is_summary_score,
149
- e.summary_eval_ids AS eval_summary_eval_ids
150
  `
151
 
152
  function normalizeDuckDBValue(value: unknown): unknown {
 
97
  // the binding only ever sees primitive types; parseMaybeJson restores
98
  // the shape on the JS side.
99
  const CELL_JOIN_COLUMNS = `
100
+ CAST(r.evaluation_id AS VARCHAR) AS evaluation_id,
101
+ CAST(r.metric_summary_id AS VARCHAR) AS metric_summary_id,
102
+ CAST(r.composite_slug AS VARCHAR) AS composite_slug,
103
+ CAST(r.composite_display_name AS VARCHAR) AS composite_display_name,
104
+ CAST(r.benchmark_id AS VARCHAR) AS benchmark_id,
105
+ CAST(r.family_id AS VARCHAR) AS family_id,
106
+ CAST(r.family_display_name AS VARCHAR) AS family_display_name,
107
+ CAST(r.is_slice AS BOOLEAN) AS is_slice,
108
+ CAST(r.parent_benchmark_id AS VARCHAR) AS parent_benchmark_id,
109
+ CAST(r.metric_id AS VARCHAR) AS metric_id,
110
+ CAST(r.model_key AS VARCHAR) AS model_key,
111
+ CAST(r.model_id AS VARCHAR) AS model_id,
112
+ CAST(r.model_route_id AS VARCHAR) AS model_route_id,
113
  CAST(to_json(r.model_info) AS VARCHAR) AS model_info,
114
  CAST(to_json(r.generation_config) AS VARCHAR) AS generation_config,
115
+ CAST(r.metric_display_name AS VARCHAR) AS metric_display_name,
116
+ CAST(r.metric_unit AS VARCHAR) AS metric_unit,
117
+ CAST(r.lower_is_better AS BOOLEAN) AS lower_is_better,
118
+ CAST(r.category AS VARCHAR) AS category,
119
+ CAST(r.score AS DOUBLE) AS score,
120
  CAST(to_json(r.score_details) AS VARCHAR) AS score_details,
121
+ CAST(r.position AS INTEGER) AS position,
122
  CAST(r.evaluation_timestamp AS VARCHAR) AS evaluation_timestamp,
123
  CAST(to_json(r.source_metadata) AS VARCHAR) AS source_metadata,
124
  CAST(to_json(r.source_data) AS VARCHAR) AS source_data,
125
+ CAST(r.source_record_url AS VARCHAR) AS source_record_url,
126
+ CAST(r.is_summary_score AS BOOLEAN) AS is_summary_score,
127
  CAST(to_json(r.aggregate_components) AS VARCHAR) AS aggregate_components,
128
  CAST(to_json(r.evalcards_annotations) AS VARCHAR) AS evalcards_annotations,
129
  CAST(to_json(r.eval_library) AS VARCHAR) AS eval_library,
130
+ CAST(r.instance_file_path AS VARCHAR) AS instance_file_path,
131
+ CAST(e.evaluation_name AS VARCHAR) AS eval_evaluation_name,
132
+ CAST(e.canonical_display_name AS VARCHAR) AS eval_canonical_display_name,
133
+ CAST(e.benchmark_id AS VARCHAR) AS eval_benchmark_id,
134
+ CAST(e.composite_slug AS VARCHAR) AS eval_composite_slug,
135
+ CAST(e.composite_display_name AS VARCHAR) AS eval_composite_display_name,
136
+ CAST(e.family_id AS VARCHAR) AS eval_family_id,
137
+ CAST(e.family_display_name AS VARCHAR) AS eval_family_display_name,
138
+ CAST(e.is_slice AS BOOLEAN) AS eval_is_slice,
139
+ CAST(e.parent_benchmark_id AS VARCHAR) AS eval_parent_benchmark_id,
140
+ CAST(e.composite_slug AS VARCHAR) AS eval_composite_benchmark_key,
141
+ CAST(e.composite_display_name AS VARCHAR) AS eval_composite_benchmark_name,
142
+ CAST(e.family_display_name AS VARCHAR) AS eval_benchmark_family_name,
143
+ CAST(e.category AS VARCHAR) AS eval_category,
144
  CAST(to_json(e.metric_config) AS VARCHAR) AS eval_metric_config,
145
  CAST(to_json(e.source_data) AS VARCHAR) AS eval_source_data,
146
  CAST(to_json(e.benchmark_card) AS VARCHAR) AS eval_benchmark_card,
147
  CAST(to_json(e.tags) AS VARCHAR) AS eval_tags,
148
+ CAST(e.is_summary_score AS BOOLEAN) AS eval_is_summary_score,
149
+ CAST(to_json(e.summary_eval_ids) AS VARCHAR) AS eval_summary_eval_ids
150
  `
151
 
152
  function normalizeDuckDBValue(value: unknown): unknown {
output/hierarchy_explorer.html ADDED
The diff for this file is too large to render. See raw diff
 
public/peer-ranks.json ADDED
The diff for this file is too large to render. See raw diff