Delete web/src/app/experiments/ExperimentsView.tsx with huggingface_hub
Browse files
web/src/app/experiments/ExperimentsView.tsx
DELETED
|
@@ -1,260 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import { useEffect, useMemo, useState } from "react";
|
| 4 |
-
import { getJSON, type Experiments, type GridRow, type PerQuery, type View } from "@/lib/api";
|
| 5 |
-
import { bytes, fmtTs, pct } from "@/lib/format";
|
| 6 |
-
import { VIEW_INFO, VIEW_ORDER } from "@/lib/views";
|
| 7 |
-
import { ClipPlayer, type ClipRef } from "@/components/ClipPlayer";
|
| 8 |
-
import { Segmented, ViewSwatch } from "@/components/Controls";
|
| 9 |
-
import { Heatmap } from "@/components/charts/Heatmap";
|
| 10 |
-
import { MetricByDimChart, type Point } from "@/components/charts/MetricByDimChart";
|
| 11 |
-
|
| 12 |
-
const METRICS = ["recall@1", "recall@5", "recall@10", "mrr@10", "neighbor_recall@5", "video_hit@1"] as const;
|
| 13 |
-
type Metric = (typeof METRICS)[number];
|
| 14 |
-
const METRIC_HELP: Record<Metric, string> = {
|
| 15 |
-
"recall@1": "Exact source segment ranked first",
|
| 16 |
-
"recall@5": "Exact source segment in the top 5",
|
| 17 |
-
"recall@10": "Exact source segment in the top 10",
|
| 18 |
-
"mrr@10": "Mean reciprocal rank of the source segment (0 if outside top 10)",
|
| 19 |
-
"neighbor_recall@5": "Source segment or an adjacent one (±30s) in the top 5",
|
| 20 |
-
"video_hit@1": "Top result comes from the right video",
|
| 21 |
-
};
|
| 22 |
-
const QTYPES = ["all", "spoken", "visual"] as const;
|
| 23 |
-
type QType = (typeof QTYPES)[number];
|
| 24 |
-
|
| 25 |
-
function Tile({ label, value, sub }: { label: string; value: string; sub?: string }) {
|
| 26 |
-
return (
|
| 27 |
-
<div className="rounded-xl border border-line bg-surface p-4">
|
| 28 |
-
<div className="text-xs text-muted">{label}</div>
|
| 29 |
-
<div className="mt-1 text-2xl font-semibold">{value}</div>
|
| 30 |
-
{sub && <div className="mt-0.5 text-xs text-ink-2">{sub}</div>}
|
| 31 |
-
</div>
|
| 32 |
-
);
|
| 33 |
-
}
|
| 34 |
-
|
| 35 |
-
function rankCell(rank: number | undefined) {
|
| 36 |
-
if (rank == null) return <td />;
|
| 37 |
-
const good = rank === 1 ? "bg-[#1c5cab] text-white" : rank <= 5 ? "bg-[#104281] text-white" : rank <= 10 ? "bg-surface-2 text-ink-2" : "text-muted";
|
| 38 |
-
return <td className={`rounded px-2 py-1 text-right text-xs tabular ${good}`}>{rank > 99 ? "99+" : rank}</td>;
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
export function ExperimentsView() {
|
| 42 |
-
const [data, setData] = useState<Experiments | null>(null);
|
| 43 |
-
const [error, setError] = useState<string | null>(null);
|
| 44 |
-
const [metric, setMetric] = useState<Metric>("recall@5");
|
| 45 |
-
const [qtype, setQtype] = useState<QType>("all");
|
| 46 |
-
const [heatMode, setHeatMode] = useState<"absolute" | "retained">("absolute");
|
| 47 |
-
const [hoverView, setHoverView] = useState<View | null>(null);
|
| 48 |
-
const [tableDim, setTableDim] = useState(768);
|
| 49 |
-
const [filter, setFilter] = useState("");
|
| 50 |
-
const [clip, setClip] = useState<ClipRef | null>(null);
|
| 51 |
-
const [transcripts, setTranscripts] = useState<string | null>(null);
|
| 52 |
-
|
| 53 |
-
useEffect(() => {
|
| 54 |
-
getJSON<Experiments>(`/api/experiments${transcripts ? `?transcripts=${transcripts}` : ""}`)
|
| 55 |
-
.then(setData)
|
| 56 |
-
.catch((e) => setError(String(e)));
|
| 57 |
-
}, [transcripts]);
|
| 58 |
-
|
| 59 |
-
const views = useMemo(() => VIEW_ORDER.filter((v) => data?.views.includes(v)), [data]);
|
| 60 |
-
const dimsAsc = useMemo(() => [...(data?.dims ?? [])].sort((a, b) => a - b), [data]);
|
| 61 |
-
const cell = useMemo(() => {
|
| 62 |
-
const m = new Map<string, GridRow>();
|
| 63 |
-
data?.grid.forEach((g) => m.set(`${g.view}|${g.dim}|${g.query_type}`, g));
|
| 64 |
-
return (view: View, dim: number) => m.get(`${view}|${dim}|${qtype}`);
|
| 65 |
-
}, [data, qtype]);
|
| 66 |
-
|
| 67 |
-
if (error) return <p className="text-sm text-ink-2">{error}</p>;
|
| 68 |
-
if (!data) return <p className="text-sm text-muted">Loading…</p>;
|
| 69 |
-
|
| 70 |
-
const points: Point[] = dimsAsc.map((d) => ({ dim: d, ...Object.fromEntries(views.map((v) => [v, cell(v, d)?.[metric] ?? 0])) }));
|
| 71 |
-
const best = data.grid.filter((g) => g.query_type === qtype).sort((a, b) => b[metric] - a[metric])[0];
|
| 72 |
-
const best768 = (v: View) => cell(v, 768)?.[metric] ?? 0;
|
| 73 |
-
const fused256 = cell("fused", 256)?.[metric];
|
| 74 |
-
const fused768 = best768("fused");
|
| 75 |
-
const bestView = views.reduce((a, v) => (best768(v) > best768(a) ? v : a), views[0]);
|
| 76 |
-
|
| 77 |
-
const queries = data.per_query.filter(
|
| 78 |
-
(q) => (qtype === "all" || q.type === qtype) && (!filter || q.query.toLowerCase().includes(filter.toLowerCase())),
|
| 79 |
-
);
|
| 80 |
-
|
| 81 |
-
return (
|
| 82 |
-
<div className="space-y-8">
|
| 83 |
-
<section>
|
| 84 |
-
<h1 className="text-2xl font-semibold">Experiments</h1>
|
| 85 |
-
<p className="mt-1 max-w-3xl text-sm text-ink-2">
|
| 86 |
-
{data.num_queries} queries ({data.query_types.spoken} spoken, {data.query_types.visual} visual) written by Gemma 4 12B after
|
| 87 |
-
watching sampled segments, each with one known source segment, run against {data.num_segments} segments for every embedding
|
| 88 |
-
view and Matryoshka dimension, with {data.transcripts === "whisper" ? "Whisper" : data.transcripts === "gemma" ? "Gemma 4" : "unlabelled"}{" "}
|
| 89 |
-
transcripts behind the text and fused views. Run {new Date(data.created_at).toLocaleString()}.
|
| 90 |
-
</p>
|
| 91 |
-
</section>
|
| 92 |
-
|
| 93 |
-
{/* Filters: one row, scope everything below */}
|
| 94 |
-
<div className="flex flex-wrap items-center gap-x-6 gap-y-3 rounded-xl border border-line bg-surface p-3">
|
| 95 |
-
<Segmented label="Metric" value={metric} options={METRICS} onChange={setMetric} />
|
| 96 |
-
<Segmented label="Queries" value={qtype} options={QTYPES} onChange={setQtype} />
|
| 97 |
-
{data.variants && data.variants.length > 1 && data.transcripts && (
|
| 98 |
-
<Segmented
|
| 99 |
-
label="Transcripts"
|
| 100 |
-
value={data.transcripts}
|
| 101 |
-
options={data.variants}
|
| 102 |
-
onChange={setTranscripts}
|
| 103 |
-
render={(e) => (e === "gemma" ? "Gemma 4" : e === "whisper" ? "Whisper" : e)}
|
| 104 |
-
/>
|
| 105 |
-
)}
|
| 106 |
-
<span className="text-xs text-muted">{METRIC_HELP[metric]}</span>
|
| 107 |
-
</div>
|
| 108 |
-
|
| 109 |
-
<section className="grid grid-cols-2 gap-3 md:grid-cols-4">
|
| 110 |
-
<Tile label={`Best config · ${metric}`} value={best ? best[metric].toFixed(3) : "–"} sub={best ? `${VIEW_INFO[best.view].label} @ ${best.dim}d` : ""} />
|
| 111 |
-
<Tile label="Best view at 768d" value={VIEW_INFO[bestView].label} sub={`${best768(bestView).toFixed(3)} ${metric}`} />
|
| 112 |
-
<Tile
|
| 113 |
-
label="Fused at 256d keeps"
|
| 114 |
-
value={fused256 != null && fused768 ? pct(fused256 / fused768) : "–"}
|
| 115 |
-
sub={`of 768d ${metric} at 1/3 the storage`}
|
| 116 |
-
/>
|
| 117 |
-
<Tile label="Query embed" value={`${data.query_embed_ms.toFixed(1)} ms`} sub="per query (batched, bf16)" />
|
| 118 |
-
</section>
|
| 119 |
-
|
| 120 |
-
<section className="rounded-xl border border-line bg-surface p-5">
|
| 121 |
-
<div className="flex flex-wrap items-baseline justify-between gap-2">
|
| 122 |
-
<h2 className="font-medium">
|
| 123 |
-
{metric} by Matryoshka dimension <span className="text-sm font-normal text-muted">· {qtype} queries</span>
|
| 124 |
-
</h2>
|
| 125 |
-
</div>
|
| 126 |
-
{/* Legend (always present for >1 series); hover a key to emphasise its line. */}
|
| 127 |
-
<div className="mt-3 flex flex-wrap gap-x-5 gap-y-1">
|
| 128 |
-
{views.map((v) => (
|
| 129 |
-
<button
|
| 130 |
-
key={v}
|
| 131 |
-
onMouseEnter={() => setHoverView(v)}
|
| 132 |
-
onMouseLeave={() => setHoverView(null)}
|
| 133 |
-
onFocus={() => setHoverView(v)}
|
| 134 |
-
onBlur={() => setHoverView(null)}
|
| 135 |
-
className="flex items-center gap-1.5 text-sm text-ink-2"
|
| 136 |
-
>
|
| 137 |
-
<span className="inline-block h-0.5 w-4 rounded" style={{ background: VIEW_INFO[v].color }} />
|
| 138 |
-
{VIEW_INFO[v].label}
|
| 139 |
-
<span className="text-xs text-muted">{VIEW_INFO[v].blurb}</span>
|
| 140 |
-
</button>
|
| 141 |
-
))}
|
| 142 |
-
</div>
|
| 143 |
-
<MetricByDimChart data={points} views={views} metric={metric} highlight={hoverView} />
|
| 144 |
-
</section>
|
| 145 |
-
|
| 146 |
-
<section className="grid gap-6 lg:grid-cols-[1fr_380px]">
|
| 147 |
-
<div className="rounded-xl border border-line bg-surface p-5">
|
| 148 |
-
<div className="mb-3 flex flex-wrap items-center justify-between gap-2">
|
| 149 |
-
<h2 className="font-medium">
|
| 150 |
-
{heatMode === "absolute" ? metric : `% of 768d ${metric} retained`}{" "}
|
| 151 |
-
<span className="text-sm font-normal text-muted">· {qtype} queries</span>
|
| 152 |
-
</h2>
|
| 153 |
-
<Segmented label="Show" value={heatMode} options={["absolute", "retained"] as const} onChange={setHeatMode} />
|
| 154 |
-
</div>
|
| 155 |
-
<Heatmap
|
| 156 |
-
views={views}
|
| 157 |
-
dims={[...dimsAsc].reverse()}
|
| 158 |
-
value={(v, d) => {
|
| 159 |
-
const x = cell(v, d)?.[metric];
|
| 160 |
-
if (x == null) return undefined;
|
| 161 |
-
if (heatMode === "absolute") return x;
|
| 162 |
-
const base = cell(v, 768)?.[metric];
|
| 163 |
-
return base ? x / base : undefined;
|
| 164 |
-
}}
|
| 165 |
-
format={(x) => (heatMode === "absolute" ? x.toFixed(3) : pct(x))}
|
| 166 |
-
/>
|
| 167 |
-
</div>
|
| 168 |
-
<div className="rounded-xl border border-line bg-surface p-5">
|
| 169 |
-
<h2 className="mb-3 font-medium">Storage (float32)</h2>
|
| 170 |
-
<table className="w-full text-sm">
|
| 171 |
-
<thead>
|
| 172 |
-
<tr className="text-xs text-muted">
|
| 173 |
-
<th className="pb-2 text-left font-normal">Dims</th>
|
| 174 |
-
<th className="pb-2 text-right font-normal">Per vector</th>
|
| 175 |
-
<th className="pb-2 text-right font-normal">Per view</th>
|
| 176 |
-
<th className="pb-2 text-right font-normal">All views</th>
|
| 177 |
-
</tr>
|
| 178 |
-
</thead>
|
| 179 |
-
<tbody className="tabular">
|
| 180 |
-
{data.storage.map((s) => (
|
| 181 |
-
<tr key={s.dim} className="border-t border-line">
|
| 182 |
-
<td className="py-2">
|
| 183 |
-
{s.dim}d <span className="text-xs text-muted">{s.dim === 768 ? "" : `÷${(768 / s.dim).toFixed(1)}`}</span>
|
| 184 |
-
</td>
|
| 185 |
-
<td className="py-2 text-right">{bytes(s.bytes_per_vector)}</td>
|
| 186 |
-
<td className="py-2 text-right">{bytes(s.bytes_per_view)}</td>
|
| 187 |
-
<td className="py-2 text-right">{bytes(s.bytes_all_views)}</td>
|
| 188 |
-
</tr>
|
| 189 |
-
))}
|
| 190 |
-
</tbody>
|
| 191 |
-
</table>
|
| 192 |
-
<p className="mt-3 text-xs text-muted">
|
| 193 |
-
Tiny at this corpus size; the ratios are what matter at scale. Model card: ~lossless to 256d, 128d drops sharply for
|
| 194 |
-
multimodal inputs.
|
| 195 |
-
</p>
|
| 196 |
-
</div>
|
| 197 |
-
</section>
|
| 198 |
-
|
| 199 |
-
<section className="rounded-xl border border-line bg-surface p-5">
|
| 200 |
-
<div className="mb-3 flex flex-wrap items-center gap-4">
|
| 201 |
-
<h2 className="font-medium">Per-query ranks</h2>
|
| 202 |
-
<Segmented label="Dims" value={tableDim} options={data.dims} onChange={setTableDim} render={(d) => `${d}d`} />
|
| 203 |
-
<input
|
| 204 |
-
value={filter}
|
| 205 |
-
onChange={(e) => setFilter(e.target.value)}
|
| 206 |
-
placeholder="Filter queries…"
|
| 207 |
-
className="ml-auto w-64 rounded-lg border border-line bg-page px-3 py-1.5 text-sm outline-none focus:border-accent"
|
| 208 |
-
/>
|
| 209 |
-
</div>
|
| 210 |
-
<p className="mb-3 text-xs text-muted">Rank of the source segment for each view (1 = perfect). Click a row to play the source segment.</p>
|
| 211 |
-
<div className="max-h-[560px] overflow-y-auto">
|
| 212 |
-
<table className="w-full border-separate text-sm" style={{ borderSpacing: "2px" }}>
|
| 213 |
-
<thead className="sticky top-0 bg-surface">
|
| 214 |
-
<tr className="text-xs text-muted">
|
| 215 |
-
<th className="py-1 text-left font-normal">Query</th>
|
| 216 |
-
<th className="py-1 text-left font-normal">Type</th>
|
| 217 |
-
{views.map((v) => (
|
| 218 |
-
<th key={v} className="py-1 text-right font-normal">
|
| 219 |
-
<span className="inline-flex items-center gap-1">
|
| 220 |
-
<ViewSwatch view={v} />
|
| 221 |
-
{VIEW_INFO[v].label}
|
| 222 |
-
</span>
|
| 223 |
-
</th>
|
| 224 |
-
))}
|
| 225 |
-
</tr>
|
| 226 |
-
</thead>
|
| 227 |
-
<tbody>
|
| 228 |
-
{queries.map((q: PerQuery, i) => (
|
| 229 |
-
<tr
|
| 230 |
-
key={i}
|
| 231 |
-
onClick={() => q.segment && setClip({ ...q.segment, text: q.evidence ? `On screen: ${q.evidence}` : undefined })}
|
| 232 |
-
className="cursor-pointer hover:bg-surface-2"
|
| 233 |
-
>
|
| 234 |
-
<td className="py-1 pr-3">
|
| 235 |
-
<div>{q.query}</div>
|
| 236 |
-
{q.segment && (
|
| 237 |
-
<div className="text-xs text-muted">
|
| 238 |
-
{q.segment.title} @ {fmtTs(q.segment.start)}
|
| 239 |
-
</div>
|
| 240 |
-
)}
|
| 241 |
-
</td>
|
| 242 |
-
<td className="py-1 pr-2 text-xs text-ink-2">{q.type}</td>
|
| 243 |
-
{views.map((v) => (
|
| 244 |
-
<RankTd key={v} rank={q.ranks[`${v}@${tableDim}`]} />
|
| 245 |
-
))}
|
| 246 |
-
</tr>
|
| 247 |
-
))}
|
| 248 |
-
</tbody>
|
| 249 |
-
</table>
|
| 250 |
-
</div>
|
| 251 |
-
</section>
|
| 252 |
-
|
| 253 |
-
<ClipPlayer clip={clip} onClose={() => setClip(null)} />
|
| 254 |
-
</div>
|
| 255 |
-
);
|
| 256 |
-
}
|
| 257 |
-
|
| 258 |
-
function RankTd({ rank }: { rank?: number }) {
|
| 259 |
-
return rankCell(rank);
|
| 260 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|