| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="utf-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> |
| <title>MR V3 SFT Results — Viewer</title> |
|
|
| |
| |
| |
| |
| <link rel="stylesheet" href="vendor/katex.min.css" /> |
| <script src="vendor/katex.min.js"></script> |
| <script src="vendor/marked.min.js"></script> |
|
|
| <style> |
| :root { |
| --bg: #0f1115; --panel: #171a21; --panel2: #1e222b; --line: #2a2f3a; |
| --fg: #e6e9ef; --muted: #9aa3b2; --accent: #6ea8fe; --accent2: #7ee0c0; |
| --warn: #ffb454; --bad: #ff6b6b; --good: #5ad18b; |
| --chip: #232838; --dir: #2a3550; --exec: #2c3d33; --final: #3a3146; |
| } |
| * { box-sizing: border-box; } |
| html, body { margin: 0; height: 100%; } |
| body { |
| background: var(--bg); color: var(--fg); font: 14px/1.55 -apple-system, |
| BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; |
| display: grid; grid-template-columns: 340px 1fr; height: 100vh; overflow: hidden; |
| } |
| |
| |
| #sidebar { background: var(--panel); border-right: 1px solid var(--line); |
| display: flex; flex-direction: column; height: 100vh; overflow: hidden; } |
| #sidebar h1 { font-size: 15px; margin: 0; padding: 14px 16px 10px; |
| border-bottom: 1px solid var(--line); letter-spacing: .3px; } |
| #sidebar h1 small { color: var(--muted); font-weight: 400; display: block; |
| font-size: 11px; margin-top: 2px; } |
| #search { margin: 10px 12px 4px; padding: 8px 10px; background: var(--panel2); |
| border: 1px solid var(--line); border-radius: 8px; color: var(--fg); width: calc(100% - 24px); } |
| #search::placeholder { color: var(--muted); } |
| #filters { display: flex; gap: 6px; flex-wrap: wrap; padding: 4px 12px 8px; |
| border-bottom: 1px solid var(--line); } |
| #filters select { background: var(--panel2); border: 1px solid var(--line); |
| color: var(--fg); border-radius: 7px; padding: 4px 6px; font-size: 11.5px; flex: 1; min-width: 0; } |
| #trajList { overflow-y: auto; flex: 1; padding: 4px 8px 16px; } |
| .trajItem { padding: 9px 10px; border-radius: 8px; cursor: pointer; margin-bottom: 4px; |
| border: 1px solid transparent; } |
| .trajItem:hover { background: var(--panel2); } |
| .trajItem.active { background: var(--panel2); border-color: var(--accent); } |
| .trajItem .tt { font-size: 12.5px; color: var(--fg); display: -webkit-box; |
| -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } |
| .trajItem .meta { font-size: 11px; color: var(--muted); margin-top: 4px; |
| display: flex; gap: 6px; flex-wrap: wrap; align-items: center; } |
| .badge { font-size: 10px; padding: 1px 6px; border-radius: 999px; background: var(--chip); |
| color: var(--muted); border: 1px solid var(--line); } |
| .badge.score { color: #0c1117; background: var(--good); border: none; font-weight: 600; } |
| .badge.score.mid { background: var(--warn); } |
| .badge.score.low { background: var(--bad); } |
| .badge.pt { border-color: #55618a; color: #aab9ef; } |
| .badge.diff-hard { border-color: var(--bad); color: #ff9d9d; } |
| .badge.diff-medium { border-color: var(--warn); color: var(--warn); } |
| .badge.diff-easy { border-color: var(--good); color: var(--good); } |
| |
| |
| #main { overflow-y: auto; height: 100vh; padding: 0 0 80px; } |
| #topbar { position: sticky; top: 0; z-index: 5; background: rgba(15,17,21,.92); |
| backdrop-filter: blur(6px); border-bottom: 1px solid var(--line); |
| padding: 12px 24px; display: flex; align-items: center; gap: 14px; flex-wrap: wrap; } |
| #topbar .traj-id { font-weight: 600; } |
| #topbar .stat { color: var(--muted); font-size: 12.5px; } |
| .toggles { margin-left: auto; display: flex; gap: 6px; flex-wrap: wrap; } |
| .toggle { font-size: 12px; padding: 5px 11px; border-radius: 999px; cursor: pointer; |
| background: var(--panel2); border: 1px solid var(--line); color: var(--muted); user-select: none; } |
| .toggle.on { color: var(--fg); border-color: var(--accent); background: #1d2740; } |
| |
| .wrap { max-width: 1080px; margin: 0 auto; padding: 0 24px; } |
| |
| |
| #problem { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; |
| padding: 18px 22px; margin: 22px 0; } |
| #problem .lbl { font-size: 11px; text-transform: uppercase; letter-spacing: 1px; |
| color: var(--accent); margin-bottom: 8px; display: flex; gap: 8px; flex-wrap: wrap; align-items: center; } |
| #problem .title { font-size: 16px; font-weight: 600; margin-bottom: 10px; } |
| #problem .body { font-size: 14.5px; } |
| .tagrow { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 12px; } |
| |
| |
| .layer { margin: 26px 0; } |
| .layer-head { display: flex; align-items: baseline; gap: 12px; margin-bottom: 10px; |
| border-bottom: 1px solid var(--line); padding-bottom: 6px; } |
| .layer-head .lnum { font-size: 18px; font-weight: 700; } |
| .layer-head .lmeta { color: var(--muted); font-size: 12px; } |
| |
| .card { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; |
| margin: 10px 0; overflow: hidden; } |
| .card.exp { border-left: 3px solid var(--accent2); } |
| .card.final { border-left: 3px solid #b694e8; } |
| .card.rubric { border-left: 3px solid var(--warn); } |
| .card.reference { border-left: 3px solid #6f86c9; } |
| .card-head { padding: 11px 16px; cursor: pointer; display: flex; align-items: center; |
| gap: 10px; user-select: none; } |
| .card-head:hover { background: var(--panel2); } |
| .card-head .ttl { font-weight: 600; font-size: 13.5px; } |
| .card-head .tag { font-size: 10.5px; padding: 1px 7px; border-radius: 999px; |
| background: var(--chip); color: var(--muted); border: 1px solid var(--line); } |
| .card-head .chev { margin-left: auto; color: var(--muted); transition: transform .15s; } |
| .card.open .card-head .chev { transform: rotate(90deg); } |
| .card-body { padding: 0 16px; max-height: 0; overflow: hidden; } |
| .card.open .card-body { padding: 4px 16px 16px; max-height: none; } |
| |
| .dir-text { background: var(--dir); border-radius: 8px; padding: 10px 13px; margin: 8px 0; |
| font-size: 13px; } |
| .subsec { margin: 12px 0 4px; font-size: 11px; text-transform: uppercase; |
| letter-spacing: .8px; color: var(--muted); display: flex; align-items: center; gap: 8px; } |
| .subsec::after { content: ""; flex: 1; height: 1px; background: var(--line); } |
| |
| .prose { font-size: 13.5px; } |
| .prose p { margin: 8px 0; } |
| .prose.scroll { max-height: 520px; overflow-y: auto; background: #12151c; |
| border: 1px solid var(--line); border-radius: 8px; padding: 12px 14px; } |
| |
| .pill { font-size: 11px; padding: 2px 9px; border-radius: 999px; border: 1px solid var(--line); |
| color: var(--muted); } |
| .pill.idx { background: var(--exec); color: var(--accent2); border: none; } |
| |
| .empty { color: var(--muted); font-style: italic; padding: 8px 0; } |
| .hidden { display: none !important; } |
| |
| code { background: #12151c; padding: 1px 5px; border-radius: 4px; font-size: 12px; } |
| pre { white-space: pre-wrap; word-break: break-word; } |
| .katex { font-size: 1.02em; } |
| |
| .katex-display { overflow-x: auto; overflow-y: hidden; padding: 2px 0; margin: 8px 0; } |
| .prose .katex-error { color: var(--warn); } |
| code.rawtex { color: var(--accent2); background: #12151c; } |
| ::-webkit-scrollbar { width: 10px; height: 10px; } |
| ::-webkit-scrollbar-thumb { background: #2b313d; border-radius: 6px; } |
| ::-webkit-scrollbar-track { background: transparent; } |
| </style> |
| </head> |
| <body> |
| <aside id="sidebar"> |
| <h1>MR V3 SFT Results<small id="trajCount">loading…</small></h1> |
| <input id="search" placeholder="filter by title / problem id / paper…" /> |
| <div id="filters"> |
| <select id="fPrompt"><option value="">all prompt types</option></select> |
| <select id="fDiff"><option value="">all difficulties</option></select> |
| <select id="fSort"> |
| <option value="score">sort: score ↓</option> |
| <option value="score_asc">sort: score ↑</option> |
| <option value="title">sort: title</option> |
| <option value="layers">sort: layers ↓</option> |
| </select> |
| </div> |
| <div id="trajList"></div> |
| </aside> |
|
|
| <main id="main"> |
| <div id="topbar"> |
| <span class="traj-id" id="trajId">—</span> |
| <span class="stat" id="trajStats"></span> |
| <div class="toggles"> |
| <span class="toggle on" data-view="explore">Exploration</span> |
| <span class="toggle" data-view="dirs">Directions</span> |
| <span class="toggle on" data-view="final">Final answer</span> |
| <span class="toggle" data-view="rubric">Rubric</span> |
| <span class="toggle" data-view="reference">Reference</span> |
| </div> |
| </div> |
| <div class="wrap" id="content"> |
| <div class="empty" style="margin-top:40px">Select a problem from the left.</div> |
| </div> |
| </main> |
|
|
| <script> |
| const DATA_DIR = "data/"; |
| const state = { |
| manifest: [], |
| traj: null, |
| filters: { q: "", prompt: "", diff: "", sort: "score" }, |
| views: { explore: true, dirs: false, final: true, rubric: false, reference: false }, |
| }; |
| |
| |
| |
| |
| |
| |
| |
| |
| function escapeHtml(s) { |
| return (s || "").replace(/[&<>]/g, c => ({ "&": "&", "<": "<", ">": ">" }[c])); |
| } |
| function renderMath(el) { } |
| |
| |
| const MATH_PATTERNS = [ |
| { re: /\$\$([\s\S]+?)\$\$/g, display: true }, |
| { re: /\\\[([\s\S]+?)\\\]/g, display: true }, |
| { re: /\\\(([\s\S]+?)\\\)/g, display: false }, |
| |
| { re: /(?<![\\$])\$(?!\$)([^\n$]+?)\$(?!\$)/g, display: false }, |
| ]; |
| |
| function renderKatexSpan(tex, display) { |
| if (window.katex) { |
| try { |
| return katex.renderToString(tex, { |
| displayMode: display, throwOnError: false, output: "html", |
| }); |
| } catch (e) { } |
| } |
| |
| return `<code class="rawtex">${escapeHtml(tex)}</code>`; |
| } |
| |
| function mdToHtml(s) { |
| if (!s) return ""; |
| |
| const spans = []; |
| let text = s; |
| for (const { re, display } of MATH_PATTERNS) { |
| text = text.replace(re, (_m, tex) => { |
| const token = `@@MATH${spans.length}@@`; |
| spans.push(renderKatexSpan(tex.trim(), display)); |
| return token; |
| }); |
| } |
| |
| let html; |
| if (window.marked) { |
| try { html = marked.parse(text, { breaks: true }); } |
| catch (e) { html = "<pre>" + escapeHtml(text) + "</pre>"; } |
| } else { |
| html = "<pre>" + escapeHtml(text) + "</pre>"; |
| } |
| |
| html = html.replace(/@@MATH(\d+)@@/g, (_m, i) => spans[+i] ?? ""); |
| return html; |
| } |
| |
| |
| function proseEl(text, scroll) { |
| const d = document.createElement("div"); |
| d.className = "prose" + (scroll ? " scroll" : ""); |
| d.innerHTML = mdToHtml(text || ""); |
| return d; |
| } |
| |
| |
| async function loadManifest() { |
| const r = await fetch(DATA_DIR + "manifest.json"); |
| state.manifest = await r.json(); |
| document.getElementById("trajCount").textContent = |
| state.manifest.length.toLocaleString() + " problems"; |
| populateFilters(); |
| renderTrajList(); |
| } |
| |
| function populateFilters() { |
| const uniq = key => [...new Set(state.manifest.map(m => m[key]).filter(Boolean))].sort(); |
| const fill = (id, vals) => { |
| const sel = document.getElementById(id); |
| for (const v of vals) { |
| const o = document.createElement("option"); |
| o.value = v; o.textContent = v; sel.appendChild(o); |
| } |
| }; |
| fill("fPrompt", uniq("prompt_type")); |
| fill("fDiff", uniq("difficulty")); |
| } |
| |
| function scoreClass(norm) { |
| if (norm == null) return ""; |
| if (norm >= 0.7) return ""; |
| if (norm >= 0.35) return "mid"; |
| return "low"; |
| } |
| |
| function sortManifest(items) { |
| const s = state.filters.sort; |
| const norm = m => m.score_normalized == null ? -1 : m.score_normalized; |
| const arr = items.slice(); |
| if (s === "score") arr.sort((a, b) => norm(b) - norm(a)); |
| else if (s === "score_asc") arr.sort((a, b) => norm(a) - norm(b)); |
| else if (s === "layers") arr.sort((a, b) => (b.n_layers || 0) - (a.n_layers || 0)); |
| else if (s === "title") arr.sort((a, b) => (a.title || "").localeCompare(b.title || "")); |
| return arr; |
| } |
| |
| function renderTrajList() { |
| const list = document.getElementById("trajList"); |
| list.innerHTML = ""; |
| const { q, prompt, diff } = state.filters; |
| const f = q.trim().toLowerCase(); |
| let items = state.manifest.filter(m => { |
| if (prompt && m.prompt_type !== prompt) return false; |
| if (diff && m.difficulty !== diff) return false; |
| if (!f) return true; |
| return (m.title || "").toLowerCase().includes(f) || |
| (m.paper_id || "").toLowerCase().includes(f) || |
| String(m.problem_id).toLowerCase().includes(f) || |
| (m.topic_tags || []).some(t => t.toLowerCase().includes(f)); |
| }); |
| items = sortManifest(items); |
| for (const m of items.slice(0, 800)) { |
| const div = document.createElement("div"); |
| div.className = "trajItem"; |
| div.dataset.file = m.file; |
| const scoreBadge = m.score != null |
| ? `<span class="badge score ${scoreClass(m.score_normalized)}">${m.score}/${m.max_score ?? "?"}</span>` : ""; |
| const ptBadge = m.prompt_type |
| ? `<span class="badge pt">${escapeHtml(m.prompt_type)}</span>` : ""; |
| const diffBadge = m.difficulty |
| ? `<span class="badge diff-${m.difficulty}">${escapeHtml(m.difficulty)}</span>` : ""; |
| div.innerHTML = ` |
| <div class="tt">${escapeHtml(m.title || "(untitled)")}</div> |
| <div class="meta"> |
| <span class="badge">${escapeHtml(String(m.problem_id))}</span> |
| <span class="badge">${m.n_layers} layers · ${m.n_explorations} exp</span> |
| ${ptBadge}${diffBadge}${scoreBadge} |
| </div>`; |
| div.onclick = () => selectTraj(m.file, div); |
| list.appendChild(div); |
| } |
| if (!items.length) list.innerHTML = `<div class="empty">no matches</div>`; |
| } |
| |
| async function selectTraj(file, el) { |
| document.querySelectorAll(".trajItem.active").forEach(n => n.classList.remove("active")); |
| if (el) el.classList.add("active"); |
| const r = await fetch(DATA_DIR + file); |
| state.traj = await r.json(); |
| renderTraj(); |
| document.getElementById("main").scrollTop = 0; |
| } |
| |
| |
| function renderTraj() { |
| const t = state.traj; |
| const content = document.getElementById("content"); |
| content.innerHTML = ""; |
| document.getElementById("trajId").textContent = t.title || `Problem ${t.problem_id}`; |
| const j = t.judge || {}; |
| const scoreTxt = j.score != null ? `${j.score}/${j.max_score ?? "?"} judge` : "ungraded"; |
| document.getElementById("trajStats").textContent = |
| `${t.problem_id} · ${t.layers.length} layers · ${t.n_explorations} explorations · ${scoreTxt}` + |
| (j.model ? ` · ${j.model}` : ""); |
| |
| |
| const prob = document.createElement("div"); |
| prob.id = "problem"; |
| const metaBadges = [ |
| t.prompt_type && `<span class="badge pt">${escapeHtml(t.prompt_type)}</span>`, |
| t.difficulty && `<span class="badge diff-${t.difficulty}">${escapeHtml(t.difficulty)}</span>`, |
| t.target_level && `<span class="badge">${escapeHtml(t.target_level)}</span>`, |
| t.paper_id && `<span class="badge">${escapeHtml(t.paper_id)}</span>`, |
| ].filter(Boolean).join(""); |
| prob.innerHTML = |
| `<div class="lbl">Problem ${escapeHtml(String(t.problem_id))} ${metaBadges}</div>` + |
| (t.title ? `<div class="title">${escapeHtml(t.title)}</div>` : "") + |
| `<div class="body"></div>`; |
| prob.querySelector(".body").innerHTML = mdToHtml(t.problem); |
| if (t.topic_tags && t.topic_tags.length) { |
| const tr = document.createElement("div"); |
| tr.className = "tagrow"; |
| tr.innerHTML = t.topic_tags.map(tag => `<span class="badge">${escapeHtml(tag)}</span>`).join(""); |
| prob.appendChild(tr); |
| } |
| content.appendChild(prob); |
| |
| |
| for (const L of t.layers) content.appendChild(renderLayer(L)); |
| |
| |
| content.appendChild(renderFinal(t)); |
| |
| content.appendChild(renderRubricReference(t)); |
| |
| applyViews(); |
| renderMath(content); |
| } |
| |
| function card(cls, headHtml, open) { |
| const c = document.createElement("div"); |
| c.className = "card " + cls + (open ? " open" : ""); |
| const h = document.createElement("div"); |
| h.className = "card-head"; |
| h.innerHTML = headHtml + `<span class="chev">▶</span>`; |
| const b = document.createElement("div"); |
| b.className = "card-body"; |
| h.onclick = () => c.classList.toggle("open"); |
| c.appendChild(h); c.appendChild(b); |
| c._body = b; |
| return c; |
| } |
| |
| function renderLayer(L) { |
| const wrap = document.createElement("div"); |
| wrap.className = "layer"; |
| wrap.dataset.view = "explore"; |
| const n = L.explorations.length; |
| const head = document.createElement("div"); |
| head.className = "layer-head"; |
| head.innerHTML = `<span class="lnum">Layer ${L.layer}</span> |
| <span class="lmeta">${n} exploration${n === 1 ? "" : "s"}</span>`; |
| wrap.appendChild(head); |
| |
| for (const ex of L.explorations) { |
| const label = (ex.label || "").trim() || `${L.layer}`; |
| const headline = ex.core_result || ex.found || ex.direction; |
| const c = card("exp", `<span class="ttl">Exploration</span> |
| <span class="pill idx">${escapeHtml(label)}</span> |
| <span class="tag">${truncate(headline, 78)}</span>`, false); |
| const b = c._body; |
| |
| |
| if (ex.direction) { |
| const dirWrap = document.createElement("div"); |
| dirWrap.dataset.view = "dirs"; |
| dirWrap.innerHTML = `<div class="subsec">Direction explored</div>`; |
| const dt = document.createElement("div"); |
| dt.className = "dir-text"; |
| dt.innerHTML = mdInline(ex.direction); |
| dirWrap.appendChild(dt); |
| b.appendChild(dirWrap); |
| } |
| |
| if (ex.found) b.appendChild(labeled("Found", ex.found)); |
| |
| if (ex.rationale) b.appendChild(labeled("Rationale", ex.rationale)); |
| |
| if (ex.core_result) b.appendChild(labeled("Core result", ex.core_result, true)); |
| wrap.appendChild(c); |
| } |
| return wrap; |
| } |
| |
| function renderFinal(t) { |
| const wrap = document.createElement("div"); |
| wrap.className = "layer"; |
| wrap.dataset.view = "final"; |
| const j = t.judge || {}; |
| const head = document.createElement("div"); |
| head.className = "layer-head"; |
| const pct = j.score_normalized != null ? ` · ${Math.round(j.score_normalized * 100)}%` : ""; |
| const scoreTag = j.score != null |
| ? `<span class="badge score ${scoreClass(j.score_normalized)}" style="font-size:12px">judge ${j.score}/${j.max_score ?? "?"}${pct}</span>` : ""; |
| head.innerHTML = `<span class="lnum">Final answer</span> |
| <span class="lmeta">${escapeHtml(t.final_kind || "")}${t.final_depth != null ? ` · depth ${t.final_depth}` : ""}${t.final_answer_source ? ` · ${escapeHtml(t.final_answer_source)}` : ""}</span>`; |
| wrap.appendChild(head); |
| |
| |
| if (j.explanation || j.score != null) { |
| const c = card("final", `<span class="ttl">Judge verdict</span> |
| ${scoreTag} |
| ${j.model ? `<span class="tag">${escapeHtml(j.model)}${j.reasoning_effort ? ` · ${escapeHtml(j.reasoning_effort)}` : ""}</span>` : ""}`, true); |
| if (j.explanation) c._body.appendChild(proseEl(j.explanation)); |
| else c._body.appendChild(proseEl(`Score: ${j.score}/${j.max_score}`)); |
| wrap.appendChild(c); |
| } |
| |
| |
| const c = card("final", `<span class="ttl">Synthesized solution</span> |
| <span class="tag">${(t.final_answer || "").length.toLocaleString()} chars</span>`, true); |
| if (t.final_answer) c._body.appendChild(proseEl(t.final_answer)); |
| else c._body.appendChild(el("div", "empty", "No final answer recorded.")); |
| wrap.appendChild(c); |
| return wrap; |
| } |
| |
| function renderRubricReference(t) { |
| const wrap = document.createElement("div"); |
| wrap.className = "layer"; |
| |
| if (t.rubric) { |
| const rw = document.createElement("div"); rw.dataset.view = "rubric"; |
| const c = card("rubric", `<span class="ttl">Grading rubric</span>`, false); |
| c._body.appendChild(proseEl(t.rubric, true)); |
| rw.appendChild(c); wrap.appendChild(rw); |
| } |
| if (t.reference_answer) { |
| const rw = document.createElement("div"); rw.dataset.view = "reference"; |
| const c = card("reference", `<span class="ttl">Reference answer</span>`, false); |
| c._body.appendChild(proseEl(t.reference_answer, true)); |
| rw.appendChild(c); wrap.appendChild(rw); |
| } |
| return wrap; |
| } |
| |
| |
| function el(tag, cls, text) { |
| const d = document.createElement(tag); |
| if (cls) d.className = cls; |
| if (text != null) d.textContent = text; |
| return d; |
| } |
| function labeled(label, text, strong) { |
| const d = document.createElement("div"); |
| d.style.margin = "8px 0"; |
| const l = document.createElement("div"); |
| l.className = "subsec"; l.style.marginTop = "0"; l.textContent = label; |
| d.appendChild(l); |
| const body = proseEl(text); |
| if (strong) body.style.color = "var(--accent2)"; |
| d.appendChild(body); |
| return d; |
| } |
| function mdInline(s) { return mdToHtml(s || "").replace(/^<p>|<\/p>\s*$/g, ""); } |
| function truncate(s, n) { s = (s || "").replace(/\s+/g, " ").trim(); |
| return s.length > n ? s.slice(0, n) + "…" : s; } |
| |
| |
| function applyViews() { |
| const v = state.views; |
| document.querySelectorAll("[data-view]").forEach(el => { |
| if (el.classList.contains("toggle")) return; |
| const key = el.dataset.view; |
| el.classList.toggle("hidden", !v[key]); |
| }); |
| document.querySelectorAll(".toggle").forEach(t => { |
| t.classList.toggle("on", v[t.dataset.view]); |
| }); |
| } |
| |
| document.querySelector(".toggles").addEventListener("click", e => { |
| const t = e.target.closest(".toggle"); |
| if (!t) return; |
| const key = t.dataset.view; |
| state.views[key] = !state.views[key]; |
| applyViews(); |
| }); |
| |
| document.getElementById("search").addEventListener("input", e => { |
| state.filters.q = e.target.value; renderTrajList(); |
| }); |
| document.getElementById("fPrompt").addEventListener("change", e => { |
| state.filters.prompt = e.target.value; renderTrajList(); |
| }); |
| document.getElementById("fDiff").addEventListener("change", e => { |
| state.filters.diff = e.target.value; renderTrajList(); |
| }); |
| document.getElementById("fSort").addEventListener("change", e => { |
| state.filters.sort = e.target.value; renderTrajList(); |
| }); |
| |
| loadManifest().catch(err => { |
| document.getElementById("trajCount").textContent = "failed to load manifest.json"; |
| document.getElementById("content").innerHTML = |
| `<div class="empty" style="margin-top:40px">Could not load <code>data/manifest.json</code>.<br>` + |
| `Run <code>build_index.py</code> first, then serve this folder with a static server ` + |
| `(e.g. <code>python -m http.server</code>).<br><br>${escapeHtml(String(err))}</div>`; |
| }); |
| </script> |
| </body> |
| </html> |
|
|