# 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//.webp`` (or ``null`` when the per-fixture status is invalid/missing, which draws the dashed "invalid generation" cell). - ``gtRenderFor(fixtureId)`` -> ``/gt-render/.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 ( "" "" "" "CADGenBench Gallery" f"" "" f"{_BODY}" f"" f"" "" ) # --------------------------------------------------------------------------- # 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 = """
Samples shown - pick up to 3 to compare across all models (changes columns globally)
""" # --------------------------------------------------------------------------- # 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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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) ? '' + esc(groupOf(f)) + '' : ''; return '' + grp + '' + esc(f ? f.name : id) + '' + '' + ''; }).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 += '
' + esc(g) + '
'; html += items.map(f => { const sel = selected.includes(f.id); const tag = f.task ? '' + esc(f.task) + '' : ''; return '
' + tag + '' + esc(f.name) + '
'; }).join(''); }); if (!html) html = '
No samples match \\u201c' + esc(popoverQuery) + '\\u201d.
'; 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', '
Max ' + MAX_FIXTURES + ' samples - remove one to add another.
'); } 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 = '
#
Submission
Score
'; selected.forEach(id => { const f = fixtureMeta(id); const task = f && f.task ? '' + esc(f.task) + '' : ''; h += '
' + esc(f ? f.name : id) + '' + task + '
'; }); 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 = '
invalid
generation
'; } function thumbHTML(url, attrs, clickable) { if (!url) { return '
invalid
generation
'; } const hint = clickable ? 'open' : ''; return '
' + '' + hint + '
'; } 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 = '
' + '
Ground truthreference geometry
' + '
1.000
'; selected.forEach(id => { gtCells += '
' + thumbHTML(gtRenderFor(id), 'data-gt="' + esc(id) + '"', false) + '
'; }); 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 ? '' + esc(s.name) + '' : '' + esc(s.name) + ''; let cells = '
' + (i + 1) + '
' + '
' + nameHTML + '' + esc(s.who) + '' + '
' + (s.blobUrl ? 'Download ZIP' : '') + '
' + '
' + fmt(s.score, 3) + '' + '
' + 'Gen' + fmt(s.gen, 3) + '' + 'Edit' + fmt(s.edit, 3) + '' + '
' + '' + pct(s.validity) + ' valid
'; selected.forEach(id => { cells += '
' + thumbHTML(gridRenderFor(s, id), 'data-sub="' + esc(s.id) + '" data-fix="' + esc(id) + '"', true) + '
'; }); 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 ? 'ground truth' : 'no GT render'; const outEl = document.getElementById('modalOut'); if (out) { outEl.className = 'mthumb'; outEl.innerHTML = 'output'; } else { outEl.className = 'mthumb failed'; outEl.innerHTML = 'invalid
generation
'; } const cad = (cell.cad === null || cell.cad === undefined) ? '-' : Number(cell.cad).toFixed(3); document.getElementById('modalNote').innerHTML = 'CAD score for this sample: ' + cad + '. 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); """