Michael Rabinovich
leaderboard: rename tabs, relabel fixtures as samples, inline gallery row stats
461547b
# Copyright 2026 Hugging Face
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Visual gallery leaderboard page.
Builds a self-contained HTML document (its own CSS + JS) from the live
submission rows. The Space serves it at ``/gallery`` and embeds it in
the Gradio "Gallery" tab via an iframe, so the bespoke visual surface
(sticky ground-truth row, fixture picker, turntable grid, report
modal) lives in plain HTML/JS isolated from Gradio's styles rather
than being forced into Gradio components.
The page is data-driven: :func:`build_gallery_payload` shapes the
top-10 verified rows + the fixture universe into a small JSON blob,
which the page's JS renders. Render lookups are isolated behind the
``renderFor`` / ``gtRenderFor`` JS hooks (mirroring the design brief),
pointed at the cached render-proxy URLs the caller injects via the two
resolvers:
- ``renderFor(sub, fixtureId)`` -> ``/render/<id>/<fixture>.webp`` (or
``null`` when the per-fixture status is invalid/missing, which draws
the dashed "invalid generation" cell).
- ``gtRenderFor(fixtureId)`` -> ``/gt-render/<fixture>.webp``.
GIFs are lazy-loaded by the browser, so only the ~33 on-screen
tiles are fetched and CDN/browser caching makes fixture-swaps and
repeat visits essentially free. This requires the Space to be
**public** (HF's edge 404s in-browser fetches to our custom routes
while private).
Turntable clicks open a GT-vs-output compare modal that points at the
existing per-submission detail/report view.
"""
from __future__ import annotations
import json
from leaderboard import _report_relative_url
# Gallery shows the top-N verified submissions only (the visual shop
# window). The numeric long tail lives on the Full results / Leaderboard
# tab, not here.
GALLERY_TOP_N = 10
# Default number of fixture columns the picker opens with, capped at the
# size of the available fixture universe. Also the hard cap on the
# picker (the matrix only stays comparable with a small column count).
DEFAULT_FIXTURE_COLUMNS = 3
def _verified_rows(rows: list[dict]) -> list[dict]:
"""Completed + validated rows, score-sorted desc, capped at the top N.
Mirrors the leaderboard's notion of "verified": ``validation_status
== 'validated'`` and a terminal ``status == 'completed'`` with a
real aggregate score. Pending / failed / unvalidated rows never
reach the visual gallery.
"""
verified = [
r
for r in rows
if r.get("validation_status") == "validated"
and r.get("status") == "completed"
and isinstance(r.get("aggregate_score"), (int, float))
]
verified.sort(key=lambda r: r.get("aggregate_score") or 0.0, reverse=True)
return verified[:GALLERY_TOP_N]
def _fixture_universe(rows: list[dict]) -> list[dict]:
"""Ordered fixture list discovered from the rows' ``per_fixture_scores``.
The fixture set is never hardcoded (it shifts as parts get added /
removed): it is the union of every ``per_fixture_scores`` key across
the verified rows, sorted for a stable column order. ``task_type``
is carried along (first non-null wins) as the small chip tag, since
difficulty tags are not available in the data.
"""
task_by_fixture: dict[str, str] = {}
for r in rows:
pfs = r.get("per_fixture_scores") or {}
for fixture_id, fx in pfs.items():
if fixture_id not in task_by_fixture:
task_by_fixture[fixture_id] = (fx or {}).get("task_type") or ""
return [
{"id": fid, "name": fid, "task": task_by_fixture[fid]}
for fid in sorted(task_by_fixture)
]
def _sub_payload(row: dict, render_resolver, diff_resolver) -> dict:
"""Project one verified row into the compact shape the page JS needs.
``render_resolver(submission_id, fixture_id)`` returns the cached
proxy URL for a *valid* fixture, or ``None``. Invalid/missing
fixtures carry ``img: null`` so the page draws the dashed cell;
note validity is driven by the per-fixture ``status`` in the data,
not by whether an image fetch happened to succeed.
Each cell also carries ``gridImg``, the source the gallery grid tile
uses: for ``editing`` fixtures this is the ghost edit-diff turntable
(``diff_resolver``) so the grid shows what actually changed; for every
other task it is the same plain candidate turntable as ``img``. The
modal keeps using ``img`` (the plain aligned output), so swapping the
grid never changes the modal.
"""
by_task = row.get("score_by_task_type") or {}
pfs = row.get("per_fixture_scores") or {}
sid = row.get("submission_id") or ""
cells: dict[str, dict] = {}
for fid, fx in pfs.items():
fx = fx or {}
status = fx.get("status") or "missing"
valid = status == "valid"
is_editing = (fx.get("task_type") or "") == "editing"
cells[fid] = {
"status": status,
"cad": fx.get("cad_score"),
"img": render_resolver(sid, fid) if valid else None,
"gridImg": (
(diff_resolver(sid, fid) if is_editing else render_resolver(sid, fid))
if valid else None
),
}
return {
"id": sid,
"name": row.get("submission_name") or "(unnamed submission)",
"reportUrl": _report_relative_url(
sid, row.get("status"), row.get("submission_sha256"),
),
"who": row.get("submitter_name") or "",
"score": row.get("aggregate_score"),
"validity": row.get("validity_rate"),
"gen": by_task.get("generation"),
"edit": by_task.get("editing"),
"date": row.get("submitted_at") or "",
"version": row.get("cadgenbench_version") or "",
"blobUrl": row.get("submission_blob_url") or "",
"cells": cells,
}
def build_gallery_payload(rows: list[dict], render_resolver, gt_resolver, diff_resolver) -> dict:
"""Shape live rows into the JSON the gallery page renders from.
Image sources are injected via resolvers so this module stays
agnostic to how the cached render URLs are constructed:
- ``render_resolver(submission_id, fixture_id) -> str | None`` (plain
candidate turntable; backs the modal and non-editing grid tiles)
- ``diff_resolver(submission_id, fixture_id) -> str | None`` (edit-diff
turntable; backs the grid tile for editing fixtures)
- ``gt_resolver(fixture_id) -> str | None``
Returns ``{"fixtures", "subs", "selected", "gtImg"}`` where
``selected`` is the default set of (up to three) fixture columns and
``gtImg`` maps each fixture to its ground-truth image source.
"""
verified = _verified_rows(rows)
fixtures = _fixture_universe(verified)
selected = [f["id"] for f in fixtures[:DEFAULT_FIXTURE_COLUMNS]]
gt_img = {f["id"]: gt_resolver(f["id"]) for f in fixtures}
return {
"fixtures": fixtures,
"subs": [_sub_payload(r, render_resolver, diff_resolver) for r in verified],
"selected": selected,
"gtImg": gt_img,
}
def render_gallery_page(rows: list[dict], render_resolver, gt_resolver, diff_resolver) -> str:
"""Build the full standalone gallery HTML document from live rows.
``render_resolver`` / ``gt_resolver`` / ``diff_resolver`` supply the
cached render-proxy URLs (see :func:`build_gallery_payload`); the
browser lazy-loads only the on-screen turntables.
The document is self-contained and uses **system font stacks only**
(no external font CDN fetch) so it never errors inside a sandboxed
iframe.
"""
payload = build_gallery_payload(rows, render_resolver, gt_resolver, diff_resolver)
data_json = json.dumps(payload, ensure_ascii=False)
return (
"<!DOCTYPE html><html lang='en'><head>"
"<meta charset='UTF-8'>"
"<meta name='viewport' content='width=device-width, initial-scale=1.0'>"
"<title>CADGenBench Gallery</title>"
f"<style>{_CSS}</style>"
"</head><body>"
f"{_BODY}"
f"<script>window.GALLERY_DATA = {data_json};</script>"
f"<script>{_JS}</script>"
"</body></html>"
)
# ---------------------------------------------------------------------------
# CSS (ported from the reference prototype, trimmed to the gallery surface).
# Self-contained: system font stacks only, no external font CDN fetch.
# ---------------------------------------------------------------------------
_CSS = """
:root {
--bg: #f4f5f7; --panel: #ffffff; --ink: #14161c; --ink-soft: #5b6170;
--ink-faint: #9aa0ad; --line: #e3e5ea; --line-strong: #d2d5dd;
--accent: #4338ca; --accent-soft: #eef0ff; --good: #15803d;
--good-soft: #e9f7ee; --bad: #b42318; --bad-soft: #fdeceb;
--gt: #0f766e; --gt-soft: #e6f4f2; --thumb-bg: #eceef2;
--shadow: 0 1px 2px rgba(20,22,28,.04), 0 8px 24px rgba(20,22,28,.06);
--radius: 14px;
--mono: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
}
* { box-sizing: border-box; }
body {
margin: 0; background: var(--bg); color: var(--ink);
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
-webkit-font-smoothing: antialiased; padding: 18px 0 60px;
}
.wrap { max-width: 1180px; margin: 0 auto; padding: 0 24px; }
.controls {
background: var(--panel); border: 1px solid var(--line);
border-radius: var(--radius); padding: 18px 20px; box-shadow: var(--shadow);
}
.controls .label {
font-size: 12px; font-weight: 700; text-transform: uppercase;
letter-spacing: .06em; color: var(--ink-faint); margin-bottom: 12px;
}
.picker-help { font-weight: 500; text-transform: none; letter-spacing: 0; color: var(--ink-faint); font-size: 12px; }
/* Compact picker: current picks as removable pills + Add + Reset */
.picker-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.pills { display: flex; gap: 8px; flex-wrap: wrap; }
.pill {
display: inline-flex; align-items: center; gap: 8px; font-size: 13.5px; font-weight: 600;
background: var(--accent-soft); border: 1px solid var(--accent); color: var(--accent);
padding: 8px 8px 8px 12px; border-radius: 10px; font-family: inherit;
}
.pill .pgroup { font-family: var(--mono); font-size: 10px; color: var(--accent); opacity: .7; }
.pill .pname { font-family: var(--mono); font-weight: 700; }
.pill .premove { cursor: pointer; border: none; background: rgba(67,56,202,.12); color: var(--accent); width: 18px; height: 18px; border-radius: 5px; font-size: 13px; line-height: 1; display: grid; place-items: center; font-family: inherit; }
.pill .premove:hover { background: var(--accent); color: #fff; }
.picker-anchor { position: relative; }
.add-fixture {
font-family: inherit; font-size: 13.5px; font-weight: 600; cursor: pointer; color: var(--ink-soft);
background: #fafbfc; border: 1px dashed var(--line-strong); padding: 9px 14px; border-radius: 10px;
transition: all .14s ease;
}
.add-fixture:hover { border-color: var(--accent); color: var(--accent); border-style: solid; }
.add-fixture:disabled { opacity: .45; cursor: not-allowed; }
.reset-fixtures {
font-family: inherit; font-size: 12.5px; font-weight: 500; cursor: pointer; color: var(--ink-faint);
background: none; border: none; padding: 9px 6px; text-decoration: underline; text-underline-offset: 2px;
}
.reset-fixtures:hover { color: var(--accent); }
/* Searchable grouped dropdown over all fixtures */
.popover {
position: absolute; top: calc(100% + 8px); left: 0; z-index: 40; width: 340px;
background: var(--panel); border: 1px solid var(--line-strong); border-radius: 12px;
box-shadow: 0 16px 40px rgba(20,22,28,.18); overflow: hidden;
}
.popover[hidden] { display: none; }
.popover-search { width: 100%; border: none; border-bottom: 1px solid var(--line); padding: 13px 15px; font-family: inherit; font-size: 14px; outline: none; }
.popover-search:focus { background: #fbfbff; }
.popover-list { max-height: 300px; overflow-y: auto; padding: 6px; }
.popover-group { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: var(--ink-faint); padding: 10px 10px 5px; position: sticky; top: 0; background: var(--panel); }
.popover-item { display: flex; align-items: center; gap: 9px; padding: 9px 10px; border-radius: 8px; cursor: pointer; font-size: 13.5px; }
.popover-item:hover { background: var(--accent-soft); }
.popover-item.is-selected { color: var(--accent); font-weight: 600; }
.popover-item.is-selected::after { content: '\\2713'; margin-left: auto; color: var(--accent); }
.popover-item .itask { font-family: var(--mono); font-size: 9px; padding: 2px 6px; border-radius: 5px; background: rgba(0,0,0,.05); text-transform: uppercase; }
.popover-item .iname { font-family: var(--mono); font-weight: 700; }
.popover-empty { padding: 18px; text-align: center; color: var(--ink-faint); font-size: 13px; }
.popover-cap { padding: 10px 12px; font-size: 11.5px; color: #b45309; background: #fdf3e7; border-top: 1px solid var(--line); }
.section-label {
display: flex; align-items: center; gap: 10px; margin: 28px 0 14px;
font-size: 14px; font-weight: 700; color: var(--accent);
text-transform: uppercase; letter-spacing: .05em;
}
.section-label .verified-pill {
font-family: var(--mono); font-size: 10px; color: var(--good);
background: var(--good-soft); padding: 3px 8px; border-radius: 999px;
letter-spacing: .02em; display: inline-flex; align-items: center; gap: 5px;
}
.dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.gallery { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); position: relative; }
.grid-head, .grow {
display: grid;
grid-template-columns: 52px minmax(220px, 1.4fr) 170px repeat(var(--ncol, 3), minmax(150px, 1fr));
align-items: stretch;
}
.grid-head {
background: #fbfbfd; border-bottom: 1px solid var(--line); font-size: 11px;
text-transform: uppercase; letter-spacing: .05em; color: var(--ink-faint);
font-weight: 700; position: sticky; top: 0; z-index: 20;
border-radius: var(--radius) var(--radius) 0 0;
}
.grid-head > div { padding: 13px 14px; display: flex; align-items: center; }
.grid-head .fix-h { flex-direction: column; align-items: flex-start; gap: 2px; }
.grid-head .fix-h .fname { color: var(--ink-soft); text-transform: none; letter-spacing: 0; font-family: var(--mono); font-size: 11px; font-weight: 700; }
.grid-head .fix-h .ftask { font-size: 9.5px; color: var(--ink-faint); text-transform: uppercase; letter-spacing: .04em; }
.grow.gt-row {
background: var(--gt-soft); border-bottom: 2px solid var(--gt);
position: sticky; top: var(--head-h, 46px); z-index: 15;
box-shadow: 0 6px 14px -8px rgba(15,118,110,.45);
}
.grow.gt-row .rank, .grow.gt-row .ident { display: flex; align-items: center; }
.grow.gt-row .ident { font-weight: 700; color: var(--gt); flex-direction: column; align-items: flex-start; justify-content: center; gap: 2px; }
.grow.gt-row .ident .gt-sub { font-weight: 500; font-size: 11.5px; color: var(--gt); opacity: .8; }
.grow.gt-row .score-cell { color: var(--gt); }
.grow.sub-row { border-bottom: 1px solid var(--line); transition: background .12s ease; }
.grow.sub-row:last-child { border-bottom: none; }
.grow.sub-row:hover { background: #fafbff; }
.rank {
padding: 16px 14px; font-family: var(--mono); font-weight: 700;
font-size: 15px; color: var(--ink-faint); display: flex; align-items: center;
justify-content: center;
}
.rank.medal-1 { color: #b8860b; } .rank.medal-2 { color: #6b7280; } .rank.medal-3 { color: #a0522d; }
.ident { padding: 14px; display: flex; flex-direction: column; justify-content: center; gap: 3px; min-width: 0; }
.ident .sub-name { font-weight: 600; font-size: 14.5px; line-height: 1.25; color: var(--ink); text-decoration: none; }
a.sub-name:hover { color: var(--accent); text-decoration: underline; }
.ident .submitter { font-size: 12px; color: var(--ink-faint); font-family: var(--mono); }
.score-cell { padding: 14px; display: flex; flex-direction: column; justify-content: center; gap: 4px; }
.score-cell .agg { font-size: 22px; font-weight: 800; letter-spacing: -.01em; }
.score-cell .validity { font-size: 11.5px; font-family: var(--mono); color: var(--good); font-weight: 700; display: flex; align-items: baseline; gap: 5px; }
.score-cell .validity .vlabel { font-weight: 400; color: var(--ink-faint); text-transform: uppercase; letter-spacing: .04em; font-size: 10px; }
.score-cell .validity.imperfect { color: #b45309; }
.score-cell .validity.imperfect .vlabel { color: #c98a3a; }
.thumb-cell { padding: 8px; display: flex; align-items: center; justify-content: center; position: relative; }
.thumb {
width: 100%; aspect-ratio: 16/10; border-radius: 8px; background: var(--thumb-bg);
border: 1px solid var(--line); overflow: hidden; cursor: pointer; position: relative;
transition: transform .14s ease, box-shadow .14s ease, border-color .14s ease;
}
.thumb:hover { transform: translateY(-2px); box-shadow: 0 6px 18px rgba(20,22,28,.14); border-color: var(--accent); }
/* Display width is CSS-constrained so the browser downscales the existing
render artifact: no resize step, no new assets. */
.thumb img { width: 100%; height: 100%; display: block; object-fit: contain; }
.thumb .open-hint {
position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
opacity: 0; background: rgba(67,56,202,.08); transition: opacity .14s ease;
font-size: 11px; font-weight: 700; color: var(--accent); text-transform: uppercase; letter-spacing: .04em;
}
.thumb:hover .open-hint { opacity: 1; }
.thumb.failed { cursor: default; background: var(--bad-soft); border: 1px dashed #e9b3ae; display: flex; align-items: center; justify-content: center; }
.thumb.failed:hover { transform: none; box-shadow: none; border-color: #e9b3ae; }
.thumb.failed .ftag { font-family: var(--mono); font-size: 10px; font-weight: 700; color: var(--bad); text-transform: uppercase; letter-spacing: .04em; text-align: center; line-height: 1.4; }
/* Inline Gen/Edit breakdown beneath the aggregate score (replaces the old
"more numbers" expander). Validity stays as its own line below. */
.score-cell .score-breakdown { display: flex; gap: 14px; margin: 1px 0; }
.score-cell .sb { display: flex; flex-direction: column; line-height: 1.15; }
.score-cell .sb-l { font-size: 9px; text-transform: uppercase; letter-spacing: .05em; color: var(--ink-faint); font-weight: 700; }
.score-cell .sb-v { font-size: 13px; font-weight: 700; font-family: var(--mono); color: var(--ink-soft); }
/* Download link + submission date, tucked under the submitter name. */
.ident .ident-foot { display: flex; align-items: center; gap: 10px; margin-top: 5px; flex-wrap: wrap; }
.ident .dl { font-size: 11.5px; font-weight: 600; color: var(--accent); text-decoration: none; display: inline-flex; align-items: center; gap: 4px; }
.ident .dl .dl-ic { font-size: 13px; line-height: 1; }
.ident .dl:hover { text-decoration: underline; }
.empty-note { background: var(--panel); border: 1px dashed var(--line-strong); border-radius: var(--radius); padding: 48px 24px; text-align: center; color: var(--ink-faint); font-size: 14px; }
/* compare modal (GT vs output) */
.modal-back { position: fixed; inset: 0; background: rgba(20,22,28,.5); backdrop-filter: blur(3px); display: none; align-items: center; justify-content: center; z-index: 50; padding: 24px; }
.modal-back.show { display: flex; }
.modal { background: var(--panel); border-radius: 16px; width: 100%; max-width: 620px; padding: 26px; box-shadow: 0 24px 60px rgba(0,0,0,.3); }
.modal h4 { margin: 0 0 4px; font-size: 18px; }
.modal .msub { color: var(--ink-faint); font-size: 13px; font-family: var(--mono); margin-bottom: 18px; }
.modal-compare { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.modal-compare figure { margin: 0; }
.modal-compare figcaption { font-size: 11px; text-transform: uppercase; letter-spacing: .05em; color: var(--ink-faint); font-weight: 700; margin-bottom: 6px; }
.modal-compare .mthumb { width: 100%; aspect-ratio: 16/10; border-radius: 8px; background: var(--thumb-bg); border: 1px solid var(--line); overflow: hidden; }
.modal-compare .mthumb img { width: 100%; height: 100%; object-fit: contain; display: block; }
.modal-compare .mthumb.failed { background: var(--bad-soft); border: 1px dashed #e9b3ae; display: flex; align-items: center; justify-content: center; }
.modal-compare .mthumb.failed span { font-family: var(--mono); font-size: 10px; font-weight: 700; color: var(--bad); text-transform: uppercase; letter-spacing: .04em; text-align: center; }
.modal-note { margin-top: 18px; font-size: 12.5px; color: var(--ink-soft); background: var(--accent-soft); padding: 12px 14px; border-radius: 10px; }
.modal-note a { color: var(--accent); font-weight: 600; }
.modal-close { margin-top: 20px; width: 100%; padding: 11px; border: 1px solid var(--line-strong); background: #fafbfc; border-radius: 10px; font-family: inherit; font-weight: 600; cursor: pointer; font-size: 14px; }
.modal-close:hover { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
"""
# ---------------------------------------------------------------------------
# Body
# ---------------------------------------------------------------------------
_BODY = """
<div class="wrap">
<div class="controls">
<div class="label">Samples shown <span class="picker-help">- pick up to 3 to compare across all models (changes columns globally)</span></div>
<div class="picker-row">
<div class="pills" id="pills"></div>
<div class="picker-anchor">
<button class="add-fixture" id="addFixtureBtn">+ Add sample</button>
<div class="popover" id="popover" hidden>
<input type="text" class="popover-search" id="popoverSearch" placeholder="Search samples..." autocomplete="off">
<div class="popover-list" id="popoverList"></div>
</div>
</div>
<button class="reset-fixtures" id="resetFixtures" title="Reset to the default comparison set">Reset</button>
</div>
</div>
<div class="section-label">
Validated leaderboard - Top 10
<span class="verified-pill"><span class="dot"></span>verified only</span>
</div>
<div class="gallery" id="gallery">
<div class="grid-head" id="gridHead"></div>
</div>
</div>
<div class="modal-back" id="modalBack">
<div class="modal">
<h4 id="modalTitle"></h4>
<div class="msub" id="modalSub"></div>
<div class="modal-compare">
<figure><figcaption>Ground truth</figcaption><div class="mthumb" id="modalGt"></div></figure>
<figure><figcaption id="modalOutCap">Output (aligned)</figcaption><div class="mthumb" id="modalOut"></div></figure>
</div>
<div class="modal-note" id="modalNote"></div>
<button class="modal-close" id="modalClose">Close</button>
</div>
</div>
"""
# ---------------------------------------------------------------------------
# JS (data-driven render of the gallery; render lookups isolated behind
# renderFor / gtRenderFor as in the design brief)
# ---------------------------------------------------------------------------
_JS = """
const DATA = window.GALLERY_DATA || {fixtures: [], subs: [], selected: [], gtImg: {}};
const FIXTURES = DATA.fixtures || [];
const MAX_FIXTURES = 3;
const DEFAULT_FIXTURES = (DATA.selected || []).slice();
let selected = DEFAULT_FIXTURES.slice();
// --- Render hooks. ---------------------------------------------------------
// The image sources are cached render-proxy URLs injected by the server, so
// these just read the payload (the browser lazy-loads only the on-screen
// tiles). renderFor returns null for an invalid/missing fixture -> dashed cell.
function renderFor(sub, fxId) {
const c = sub.cells[fxId];
return c ? c.img : null;
}
// Grid tiles use gridImg (the edit-diff turntable for editing fixtures, the
// plain candidate turntable otherwise); the modal keeps renderFor (img), so
// the grid swap never changes the modal.
function gridRenderFor(sub, fxId) {
const c = sub.cells[fxId];
return c ? (c.gridImg || c.img) : null;
}
function gtRenderFor(fxId) {
return (DATA.gtImg || {})[fxId] || null;
}
function cellOf(sub, fxId) { return sub.cells[fxId] || {}; }
function fmt(x, d) { return (x === null || x === undefined) ? '-' : Number(x).toFixed(d); }
function pct(x) { return (x === null || x === undefined) ? '-' : Math.round(Number(x) * 100) + '%'; }
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); }
function fixtureMeta(id) { return FIXTURES.find(f => f.id === id); }
function groupLabel(task) { return task ? (task.charAt(0).toUpperCase() + task.slice(1)) : 'Other'; }
function groupOf(f) { return groupLabel(f ? f.task : ''); }
// Distinct group labels in fixture order (e.g. Generation, Editing).
const GROUPS = (() => {
const seen = [];
FIXTURES.forEach(f => { const g = groupOf(f); if (!seen.includes(g)) seen.push(g); });
return seen;
})();
// --- URL persistence: ?fixtures=a,b,c -------------------------------------
// Wrapped in try/catch: history.replaceState and the URL read both throw in
// sandboxed iframe contexts (this caused an "Uncaught Error: Script error.").
function loadSelectedFromURL() {
try {
const p = new URLSearchParams(location.search).get('fixtures');
if (!p) return;
const ids = p.split(',').map(s => s.trim())
.filter(id => FIXTURES.some(f => f.id === id)).slice(0, MAX_FIXTURES);
if (ids.length) selected = ids;
} catch (e) { /* sandboxed context -> keep defaults */ }
}
function syncURL() {
try {
const u = new URL(location.href);
u.searchParams.set('fixtures', selected.join(','));
history.replaceState(null, '', u);
} catch (e) { /* sandboxed/cross-origin context -> URL persistence no-ops */ }
}
// --- Fixture picker: pills + searchable grouped dropdown -------------------
function renderPills() {
const wrap = document.getElementById('pills');
if (!selected.length) { wrap.innerHTML = ''; return; }
wrap.innerHTML = selected.map(id => {
const f = fixtureMeta(id);
const grp = (f && f.task) ? '<span class="pgroup">' + esc(groupOf(f)) + '</span>' : '';
return '<span class="pill">' + grp
+ '<span class="pname">' + esc(f ? f.name : id) + '</span>'
+ '<button class="premove" data-remove="' + esc(id) + '" title="Remove" aria-label="Remove ' + esc(id) + '">\\u00d7</button>'
+ '</span>';
}).join('');
wrap.querySelectorAll('.premove').forEach(b => {
b.onclick = () => {
if (selected.length <= 1) return; // keep at least 1 column
selected = selected.filter(x => x !== b.dataset.remove);
refreshPicker(); buildGallery(); syncURL();
};
});
}
let popoverQuery = '';
function renderPopoverList() {
const list = document.getElementById('popoverList');
const q = popoverQuery.trim().toLowerCase();
const match = f => !q || f.name.toLowerCase().includes(q)
|| groupOf(f).toLowerCase().includes(q) || (f.task || '').toLowerCase().includes(q);
let html = '';
GROUPS.forEach(g => {
const items = FIXTURES.filter(f => groupOf(f) === g && match(f));
if (!items.length) return;
html += '<div class="popover-group">' + esc(g) + '</div>';
html += items.map(f => {
const sel = selected.includes(f.id);
const tag = f.task ? '<span class="itask">' + esc(f.task) + '</span>' : '';
return '<div class="popover-item ' + (sel ? 'is-selected' : '') + '" data-pick="' + esc(f.id) + '">'
+ tag + '<span class="iname">' + esc(f.name) + '</span></div>';
}).join('');
});
if (!html) html = '<div class="popover-empty">No samples match \\u201c' + esc(popoverQuery) + '\\u201d.</div>';
list.innerHTML = html;
// At the cap, show a note rather than silently dropping a pick.
const existingCap = document.getElementById('popoverCap');
if (existingCap) existingCap.remove();
if (selected.length >= MAX_FIXTURES) {
list.insertAdjacentHTML('afterend',
'<div class="popover-cap" id="popoverCap">Max ' + MAX_FIXTURES + ' samples - remove one to add another.</div>');
}
list.querySelectorAll('.popover-item').forEach(it => {
it.onclick = () => {
const id = it.dataset.pick;
if (selected.includes(id)) {
if (selected.length <= 1) return; // keep at least 1
selected = selected.filter(x => x !== id);
} else {
if (selected.length >= MAX_FIXTURES) return; // hard cap; user removes to add
selected.push(id);
}
refreshPicker(); buildGallery(); syncURL();
};
});
}
function refreshPicker() {
renderPills();
renderPopoverList();
const add = document.getElementById('addFixtureBtn');
if (add) add.disabled = !FIXTURES.length;
}
function openPopover() {
const pop = document.getElementById('popover');
pop.hidden = false;
popoverQuery = '';
document.getElementById('popoverSearch').value = '';
renderPopoverList();
document.getElementById('popoverSearch').focus();
}
function closePopover() { document.getElementById('popover').hidden = true; }
function wirePicker() {
document.getElementById('addFixtureBtn').onclick = (e) => {
e.stopPropagation();
const pop = document.getElementById('popover');
pop.hidden ? openPopover() : closePopover();
};
document.getElementById('popoverSearch').oninput = (e) => { popoverQuery = e.target.value; renderPopoverList(); };
document.getElementById('resetFixtures').onclick = () => {
selected = DEFAULT_FIXTURES.slice(); refreshPicker(); buildGallery(); syncURL(); closePopover();
};
// click-outside closes the popover
document.addEventListener('click', (e) => {
const anchor = document.querySelector('.picker-anchor');
if (anchor && !anchor.contains(e.target)) closePopover();
});
}
// --- Gallery render -------------------------------------------------------
function buildHead() {
const head = document.getElementById('gridHead');
let h = '<div>#</div><div>Submission</div><div>Score</div>';
selected.forEach(id => {
const f = fixtureMeta(id);
const task = f && f.task ? '<span class="ftask">' + esc(f.task) + '</span>' : '';
h += '<div class="fix-h"><span class="fname">' + esc(f ? f.name : id) + '</span>' + task + '</div>';
});
head.innerHTML = h;
}
// Fall back to the dashed cell if a render URL 404s (a fixture marked valid
// whose render upload is missing) instead of showing a broken image.
function imgFail(img) {
const cell = img.closest('.thumb-cell');
if (cell) cell.innerHTML = '<div class="thumb failed"><span class="ftag">invalid<br>generation</span></div>';
}
function thumbHTML(url, attrs, clickable) {
if (!url) {
return '<div class="thumb failed"><span class="ftag">invalid<br>generation</span></div>';
}
const hint = clickable ? '<span class="open-hint">open</span>' : '';
return '<div class="thumb" ' + attrs + '>'
+ '<img loading="lazy" decoding="async" src="' + url + '" alt="" onerror="imgFail(this)">'
+ hint + '</div>';
}
function buildGallery() {
const g = document.getElementById('gallery');
g.style.setProperty('--ncol', Math.max(selected.length, 1));
buildHead();
g.querySelectorAll('.grow').forEach(n => n.remove());
if (!DATA.subs.length) {
let note = g.querySelector('.empty-note');
if (!note) {
note = document.createElement('div');
note.className = 'empty-note';
note.textContent = 'No verified submissions yet. Once a submission is promoted to the validated tier it appears here.';
g.appendChild(note);
}
return;
}
// Ground-truth pinned row.
const gt = document.createElement('div');
gt.className = 'grow gt-row';
let gtCells = '<div class="rank">&#9733;</div>'
+ '<div class="ident">Ground truth<span class="gt-sub">reference geometry</span></div>'
+ '<div class="score-cell"><span class="agg">1.000</span></div>';
selected.forEach(id => {
gtCells += '<div class="thumb-cell">' + thumbHTML(gtRenderFor(id), 'data-gt="' + esc(id) + '"', false) + '</div>';
});
gt.innerHTML = gtCells;
g.appendChild(gt);
DATA.subs.forEach((s, i) => {
const row = document.createElement('div');
row.className = 'grow sub-row';
const medal = i < 3 ? 'medal-' + (i + 1) : '';
const imperfect = (s.validity !== null && s.validity < 1) ? 'imperfect' : '';
const nameHTML = s.reportUrl
? '<a class="sub-name" href="' + esc(s.reportUrl) + '" target="_blank" rel="noopener">' + esc(s.name) + '</a>'
: '<span class="sub-name">' + esc(s.name) + '</span>';
let cells = '<div class="rank ' + medal + '">' + (i + 1) + '</div>'
+ '<div class="ident">' + nameHTML
+ '<span class="submitter">' + esc(s.who) + '</span>'
+ '<div class="ident-foot">'
+ (s.blobUrl ? '<a class="dl" href="' + esc(s.blobUrl) + '" target="_blank" rel="noopener"><span class="dl-ic">&#8675;</span>Download ZIP</a>' : '')
+ '</div></div>'
+ '<div class="score-cell"><span class="agg">' + fmt(s.score, 3) + '</span>'
+ '<div class="score-breakdown">'
+ '<span class="sb"><span class="sb-l">Gen</span><span class="sb-v">' + fmt(s.gen, 3) + '</span></span>'
+ '<span class="sb"><span class="sb-l">Edit</span><span class="sb-v">' + fmt(s.edit, 3) + '</span></span>'
+ '</div>'
+ '<span class="validity ' + imperfect + '">' + pct(s.validity) + ' <span class="vlabel">valid</span></span></div>';
selected.forEach(id => {
cells += '<div class="thumb-cell">' + thumbHTML(gridRenderFor(s, id), 'data-sub="' + esc(s.id) + '" data-fix="' + esc(id) + '"', true) + '</div>';
});
row.innerHTML = cells;
g.appendChild(row);
});
wireGallery();
syncHeadHeight();
}
function wireGallery() {
document.querySelectorAll('.thumb[data-sub]').forEach(th => {
th.onclick = () => {
const sub = DATA.subs.find(x => x.id === th.dataset.sub);
openModal(th.dataset.fix, sub);
};
});
}
function openModal(fxId, sub) {
document.getElementById('modalTitle').textContent = fxId;
document.getElementById('modalSub').textContent = sub.name;
const gt = gtRenderFor(fxId);
const out = renderFor(sub, fxId);
const cell = cellOf(sub, fxId);
document.getElementById('modalGt').innerHTML = gt
? '<img src="' + gt + '" alt="ground truth">' : '<span>no GT render</span>';
const outEl = document.getElementById('modalOut');
if (out) {
outEl.className = 'mthumb';
outEl.innerHTML = '<img src="' + out + '" alt="output">';
} else {
outEl.className = 'mthumb failed';
outEl.innerHTML = '<span>invalid<br>generation</span>';
}
const cad = (cell.cad === null || cell.cad === undefined) ? '-' : Number(cell.cad).toFixed(3);
document.getElementById('modalNote').innerHTML =
'CAD score for this sample: <b>' + cad + '</b>. The full per-sample report '
+ '(shape similarity, interface, topology + 3D view) opens from the report viewer.';
document.getElementById('modalBack').classList.add('show');
}
function closeModal() {
document.getElementById('modalBack').classList.remove('show');
}
document.getElementById('modalClose').onclick = closeModal;
document.getElementById('modalBack').onclick = (e) => { if (e.target.id === 'modalBack') closeModal(); };
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeModal(); });
// Pin the GT row exactly beneath the sticky column header.
function syncHeadHeight() {
const head = document.getElementById('gridHead');
if (head) document.documentElement.style.setProperty('--head-h', head.offsetHeight + 'px');
}
loadSelectedFromURL();
wirePicker();
refreshPicker();
buildGallery();
syncURL();
window.addEventListener('resize', syncHeadHeight);
if (document.fonts && document.fonts.ready) document.fonts.ready.then(syncHeadHeight);
"""