Download app.js from asingh15/wikirace-value-explorer: direct link, hf CLI and curl.
- Browser
- Download file 20.8 kB
-
https://huggingface.co/spaces/asingh15/wikirace-value-explorer/resolve/main/app.js
- Command line
-
hf download hf://spaces/asingh15/wikirace-value-explorer/app.js
-
curl -L -o app.js https://huggingface.co/spaces/asingh15/wikirace-value-explorer/resolve/main/app.js
20.8 kB
| ; | |
| const $ = (id) => document.getElementById(id); | |
| const state = {index:null, episode:null, activeStep:null, query:null, tab:"trajectory", cache:new Map(), loadId:0}; | |
| const number = (x, digits=3) => typeof x === "number" ? x.toFixed(digits) : "—"; | |
| const text = (tag, value, cls) => { | |
| const node = document.createElement(tag); | |
| node.textContent = value; | |
| if (cls) node.className = cls; | |
| return node; | |
| }; | |
| function node(tag, cls, children=[]) { | |
| const element = document.createElement(tag); | |
| if (cls) element.className = cls; | |
| for (const child of children) if (child) element.append(child); | |
| return element; | |
| } | |
| function button(label, cls, click) { | |
| const element = text("button", label, cls); | |
| element.type = "button"; | |
| element.addEventListener("click", click); | |
| return element; | |
| } | |
| function badge(label, color="") { return text("span", label, "badge " + color); } | |
| function stat(label, value, detail, color="") { | |
| return node("div", "stat" + (color === "green" ? " highlight" : ""), [ | |
| text("div", label, "stat-label"), text("div", value, "stat-value " + color), | |
| text("div", detail, "stat-detail") | |
| ]); | |
| } | |
| function heading(title, subtitle) { | |
| return node("div", "panel-heading", [text("h3", title), text("small", subtitle)]); | |
| } | |
| function showError(error) { | |
| $("error").hidden = false; | |
| $("error").textContent = "Could not load this view. " + error.message; | |
| } | |
| function filteredEpisodes() { | |
| const search = $("search").value.toLowerCase().trim(); | |
| return state.index.episodes.filter((e) => | |
| (!search || (e.source + " " + e.target).toLowerCase().includes(search)) && | |
| (!$("split-filter").value || e.split === $("split-filter").value) && | |
| (!$("outcome-filter").value || e.success === ($("outcome-filter").value === "success")) | |
| ); | |
| } | |
| function renderList() { | |
| const episodes = filteredEpisodes(); | |
| $("list-count").textContent = episodes.length; | |
| $("episode-list").replaceChildren(); | |
| for (const episode of episodes) { | |
| const item = button("", "episode-button" + (state.episode?.id === episode.id ? " selected" : ""), () => loadEpisode(episode.id)); | |
| item.dataset.episode = episode.id; | |
| item.append(node("span", "episode-route", [ | |
| text("strong", episode.source), text("small", "↳ " + episode.target) | |
| ])); | |
| item.append(node("span", "episode-meta", [ | |
| node("span", "", [node("i", "status-dot" + (episode.success ? " success" : "")), text("span", episode.success ? "Success" : episode.termination)]), | |
| text("span", episode.labels + " labels · " + episode.queries + " Q calls") | |
| ])); | |
| $("episode-list").append(item); | |
| } | |
| if (!episodes.length) $("episode-list").append(text("div", "No matching trajectories.", "empty")); | |
| } | |
| async function loadEpisode(id, queryIndex=null) { | |
| const generation = ++state.loadId; | |
| const snapshot = state.index.summary.created_at; | |
| const cacheKey = snapshot + "/" + id; | |
| $("error").hidden = true; | |
| try { | |
| if (!state.cache.has(cacheKey)) { | |
| $("trajectory-view").replaceChildren(text("div", "Loading trajectory…", "loading")); | |
| const response = await fetch("data/episodes/" + encodeURIComponent(id) + ".json.gz?snapshot=" + encodeURIComponent(snapshot)); | |
| if (!response.ok) throw new Error("Trajectory request returned " + response.status); | |
| let bytes = new Uint8Array(await response.arrayBuffer()); | |
| if (bytes[0] === 31 && bytes[1] === 139) { | |
| const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream("gzip")); | |
| bytes = new Uint8Array(await new Response(stream).arrayBuffer()); | |
| } | |
| state.cache.set(cacheKey, JSON.parse(new TextDecoder().decode(bytes))); | |
| } | |
| if (generation !== state.loadId) return; | |
| state.episode = state.cache.get(cacheKey); | |
| const queries = Object.values(state.episode.queries); | |
| const selected = queryIndex !== null ? state.episode.queries[String(queryIndex)] : | |
| queries.find((q) => q.target_value !== null) || queries[0]; | |
| state.activeStep = selected ? state.episode.steps.find((s) => s.query_index === selected.query_index) : state.episode.steps[0]; | |
| state.query = selected || null; | |
| setTab("trajectory"); | |
| renderTrajectory(); | |
| renderList(); | |
| } catch (error) { showError(error); } | |
| } | |
| function actionTitle(step) { | |
| const action = step.action; | |
| if (step.forced) return "Forced " + action.type + (action.link_id ? " · L" + action.link_id : ""); | |
| if (action.type === "estimate_q") return "Q call" + (step.query_index ? " #" + step.query_index : "") + " · " + | |
| (action.action?.type === "back" ? "back" : "L" + (action.action?.link_id ?? "?")); | |
| if (action.type === "click") return "Click · L" + action.link_id; | |
| if (action.type === "read_chunk") return "Read chunk " + action.index; | |
| return action.type === "back" ? "Go back" : action.type; | |
| } | |
| function icon(step) { return step.action.type === "estimate_q" ? "Q" : step.action.type === "read_chunk" ? "≡" : step.action.type === "back" ? "↶" : "↗"; } | |
| function updateHash() { | |
| const params = new URLSearchParams({episode:state.episode.id}); | |
| if (state.query) params.set("query", state.query.query_index); | |
| history.replaceState(null, "", "#" + params); | |
| } | |
| function selectStep(step) { | |
| state.activeStep = step; | |
| state.query = step.query_index ? state.episode.queries[String(step.query_index)] : null; | |
| for (const item of document.querySelectorAll(".step-button")) { | |
| item.classList.toggle("active", Number(item.dataset.decision) === step.decision); | |
| } | |
| renderStats(); | |
| renderDetail(); | |
| updateHash(); | |
| } | |
| function renderTrajectory() { | |
| const episode = state.episode; | |
| const title = node("h2", "", [text("span", episode.source), text("span", "→", "route-arrow"), text("span", episode.target)]); | |
| const header = node("div", "episode-header", [ | |
| node("div", "", [title, node("div", "episode-subtitle", [ | |
| text("span", episode.split === "train" ? "Training split" : "Validation split"), | |
| text("span", "Seed " + episode.seed), text("span", episode.navigation_steps + " / " + (state.index.summary.horizon ?? 10) + " navigation moves"), | |
| text("span", episode.decisions + " decisions") | |
| ])]), badge(episode.success ? "✓ Goal reached" : episode.termination, episode.success ? "green" : "") | |
| ]); | |
| const stats = node("div", "stats"); stats.id = "episode-stats"; | |
| const timeline = node("div", "timeline"); | |
| for (const step of episode.steps) { | |
| const query = step.query_index ? episode.queries[String(step.query_index)] : null; | |
| const item = button("", "step-button" + (state.activeStep === step ? " active" : ""), () => selectStep(step)); | |
| item.dataset.decision = step.decision; | |
| const copy = node("div", "step-copy", [ | |
| text("div", actionTitle(step), "step-title"), text("div", step.after?.page || "", "step-page") | |
| ]); | |
| if (query) copy.append(node("div", "step-values", [ | |
| badge("Pred " + number(query.predicted_value), "purple"), | |
| badge(query.target_value !== null ? "MC " + number(query.target_value) : "No MC target", query.target_value !== null ? "green" : "") | |
| ])); | |
| item.append(text("span", icon(step), "step-icon" + (query ? " query" : "")), copy, text("span", String(step.decision + 1).padStart(2, "0"), "step-number")); | |
| timeline.append(item); | |
| } | |
| const left = node("section", "panel", [heading("Parent trajectory", episode.steps.length + " actions"), timeline]); | |
| const right = node("section", "panel"); right.id = "detail-panel"; | |
| $("trajectory-view").replaceChildren(header, stats, node("div", "explorer-grid", [left, right])); | |
| renderStats(); renderDetail(); updateHash(); | |
| } | |
| function renderStats() { | |
| const q = state.query, e = state.episode; | |
| $("episode-stats").replaceChildren( | |
| stat("Prompted prediction", q ? number(q.predicted_value) : "—", q ? "Q call #" + q.query_index : "Select a Q call", "purple"), | |
| stat("Monte Carlo target", q ? number(q.target_value) : "—", q?.target_value !== null && q ? q.successes + " / " + q.samples + " successful branches" : "Selected calls only", "green"), | |
| stat("Absolute difference", q?.predicted_value != null && q.target_value != null ? number(Math.abs(q.predicted_value - q.target_value)) : "—", "Prediction vs. MC fraction"), | |
| stat("Labeled Q calls", Object.values(e.queries).filter((q) => q.target_value !== null).length + " / " + Object.keys(e.queries).length, "At most two labels per trajectory") | |
| ); | |
| } | |
| function bar(label, value, mc=false) { | |
| const fill = node("div", "fill" + (mc ? " mc" : "")); | |
| fill.style.width = (value === null ? 0 : value * 100) + "%"; | |
| return node("div", "value-bar", [text("span", label), node("div", "track", [fill]), text("strong", number(value))]); | |
| } | |
| function renderDetail() { | |
| const panel = $("detail-panel"), q = state.query, step = state.activeStep; | |
| if (!step) { panel.replaceChildren(text("div", "No recorded actions.", "empty")); return; } | |
| const selector = node("select", "query-selector"); | |
| selector.setAttribute("aria-label", "Select Q call"); | |
| selector.append(new Option("Select a Q call", "")); | |
| for (const query of Object.values(state.episode.queries)) { | |
| selector.append(new Option("Q #" + query.query_index + (query.target_value !== null ? " · MC " + number(query.target_value) : " · no target"), String(query.query_index))); | |
| } | |
| selector.value = q ? String(q.query_index) : ""; | |
| selector.addEventListener("change", () => { | |
| const selected = state.episode.steps.find((s) => String(s.query_index) === selector.value); | |
| if (selected) selectStep(selected); | |
| }); | |
| const top = node("div", "panel-heading", [text("h3", q ? "Value call #" + q.query_index : "Decision " + (step.decision + 1)), selector]); | |
| const body = node("div", "query-detail", [ | |
| text("div", q ? "PROPOSED ACTION · BEFORE NAVIGATION" : "RECORDED ACTION", "section-kicker"), | |
| text("h3", q ? (q.action?.type === "back" ? "Back" : "Click L" + (q.action?.link_id ?? "?")) + | |
| (q.candidate ? " → " + q.candidate.destination : "") : actionTitle(step), "query-title"), | |
| text("p", "At " + (step.before?.page || step.after.page) + " · " + (step.before?.remaining_steps ?? "—") + " navigation moves remaining", "description") | |
| ]); | |
| if (q) { | |
| body.append(node("div", "value-bars", [bar("Prompted Q", q.predicted_value), bar("MC target", q.target_value, true)])); | |
| body.append(text("p", q.target_value !== null ? | |
| "The parent received the prompted estimate. Independent MC branches supplied the training target afterward." : | |
| "This call was not selected for MC labeling. Its prompted estimate is shown for context; it is not a training target.", "description")); | |
| } | |
| panel.replaceChildren(top, body); | |
| if (q?.target_value !== null && q) renderBranches(panel, q); | |
| const inspector = node("section", "inspector"); inspector.id = "inspector"; | |
| panel.append(inspector); | |
| inspectStep(step, "Parent decision " + (step.decision + 1)); | |
| } | |
| function renderBranches(panel, query) { | |
| const header = heading("Continuation branches", query.successes + " of " + query.samples + " reached the goal"); | |
| header.classList.add("branch-header"); | |
| const area = node("div", "branch-area", [ | |
| text("div", state.activeStep.before.page + " · Q #" + query.query_index, "fork-root"), | |
| text("div", "Force proposed action, then follow the prompted-Q policy", "fork-label") | |
| ]); | |
| const lanes = node("div", "branch-lanes"); | |
| for (const branch of query.branches) { | |
| const outcome = node("div", "branch-outcome", [ | |
| text("strong", "Branch " + (branch.index + 1)), | |
| badge(branch.success ? "✓ Success" : "× " + branch.termination, branch.success ? "green" : "red"), | |
| text("small", "Seed " + branch.seed) | |
| ]); | |
| const flow = node("div", "branch-flow"); | |
| for (const [i, step] of branch.steps.entries()) { | |
| if (i) flow.append(text("span", "→", "flow-arrow")); | |
| const item = button("", "branch-node" + (step.forced ? " forced" : step.action.type === "estimate_q" ? " tool" : ""), () => { | |
| document.querySelectorAll(".branch-node.selected").forEach((n) => n.classList.remove("selected")); | |
| item.classList.add("selected"); | |
| inspectStep(step, "Branch " + (branch.index + 1) + " · decision " + (step.decision + 1)); | |
| }); | |
| item.append(text("small", actionTitle(step)), text("strong", step.after?.page || "")); | |
| if (step.action.type === "estimate_q") item.append(badge("Pred " + number(step.predicted_value), "purple")); | |
| flow.append(item); | |
| } | |
| if (!branch.steps.length) flow.append(text("span", "Terminal at query state: " + branch.termination, "description")); | |
| lanes.append(node("div", "branch-row", [outcome, flow])); | |
| } | |
| area.append(lanes, text("div", "Purple nodes are prompted Q calls. Green-bordered nodes are forced actions. Click any node to inspect its observation and tool result. Branch Q calls never launch more MC.", "branch-legend")); | |
| panel.append(header, area); | |
| } | |
| function inspectStep(step, label) { | |
| const inspector = $("inspector"); | |
| const observation = state.episode.observations[step.observation]; | |
| inspector.replaceChildren( | |
| text("h4", label), | |
| node("div", "inspector-grid", [ | |
| node("div", "", [text("div", "Action", "section-kicker"), text("pre", JSON.stringify(step.action, null, 2))]), | |
| node("div", "", [text("div", "Result", "section-kicker"), text("pre", JSON.stringify(step.result, null, 2))]) | |
| ]) | |
| ); | |
| const states = node("details", "", [text("summary", "Game state and remaining budgets"), text("pre", JSON.stringify({before:step.before, after:step.after}, null, 2))]); | |
| inspector.append(states); | |
| if (observation) { | |
| const detail = node("details", "", [text("summary", "Visible article · " + observation.page + (observation.chunk !== undefined ? " · chunk " + observation.chunk : ""))]); | |
| if (observation.url?.startsWith("https://en.wikipedia.org/wiki/")) { | |
| const link = text("a", "View source article on Wikipedia ↗"); | |
| link.href = observation.url; link.target = "_blank"; link.rel = "noopener"; | |
| detail.append(link); | |
| } | |
| detail.append(text("pre", observation.text || "", "article")); | |
| inspector.append(detail); | |
| } | |
| } | |
| function svgNode(tag, attributes={}) { | |
| const element = document.createElementNS("http://www.w3.org/2000/svg", tag); | |
| for (const [key, value] of Object.entries(attributes)) element.setAttribute(key, value); | |
| return element; | |
| } | |
| function renderCalibration() { | |
| const ids = new Set(filteredEpisodes().map((e) => e.id)); | |
| const points = state.index.comparisons.filter((p) => ids.has(p.episode_id)); | |
| const mae = points.length ? points.reduce((a, p) => a + Math.abs(p.predicted - p.target), 0) / points.length : null; | |
| const mse = points.length ? points.reduce((a, p) => a + (p.predicted - p.target) ** 2, 0) / points.length : null; | |
| const stats = node("div", "stats", [ | |
| stat("Compared calls", points.length, "Complete MC labels"), | |
| stat("Mean absolute error", number(mae, 3), "Against MC fractions"), | |
| stat("Mean squared error", number(mse, 3), "Against noisy MC targets"), | |
| stat("Continuations", state.index.summary.samples, "Per selected call", "green") | |
| ]); | |
| const chart = svgNode("svg", {viewBox:"0 0 600 450", class:"scatter", role:"img", "aria-label":"Prompted predictions versus Monte Carlo targets"}); | |
| for (const t of [0, .25, .5, .75, 1]) { | |
| const x = 65 + t * 475, y = 365 - t * 315; | |
| chart.append(svgNode("line", {x1:65, x2:540, y1:y, y2:y, stroke:"#e5eae0"})); | |
| chart.append(svgNode("line", {x1:x, x2:x, y1:50, y2:365, stroke:"#edf0e9"})); | |
| const xt = svgNode("text", {x:x, y:388, "text-anchor":"middle"}); xt.textContent = t.toFixed(2); chart.append(xt); | |
| const yt = svgNode("text", {x:50, y:y+4, "text-anchor":"end"}); yt.textContent = t.toFixed(2); chart.append(yt); | |
| } | |
| chart.append(svgNode("line", {x1:65, y1:365, x2:540, y2:50, stroke:"#9bb397", "stroke-dasharray":"5 5"})); | |
| for (const p of points) { | |
| const circle = svgNode("circle", {cx:65+p.predicted*475, cy:365-p.target*315, r:5.5, class:"point", fill:p.split === "train" ? "#4a987e" : "#9270b8", tabindex:0, role:"button", "aria-label":"Open Q call " + p.query_index + ", predicted " + p.predicted + ", target " + p.target}); | |
| const title = svgNode("title"); title.textContent = "Predicted " + number(p.predicted) + " · target " + number(p.target) + " · " + p.split; | |
| circle.append(title); | |
| circle.addEventListener("click", () => loadEpisode(p.episode_id, p.query_index)); | |
| circle.addEventListener("keydown", (e) => { if (e.key === "Enter") loadEpisode(p.episode_id, p.query_index); }); | |
| chart.append(circle); | |
| } | |
| const xlabel = svgNode("text", {x:302, y:427, "text-anchor":"middle"}); xlabel.textContent = "Prompted Q prediction"; | |
| const ylabel = svgNode("text", {transform:"translate(17 210) rotate(-90)", "text-anchor":"middle"}); ylabel.textContent = "MC target · observed success fraction"; | |
| chart.append(xlabel, ylabel); | |
| const plot = node("section", "panel", [heading("Predictions vs. MC targets", "Click a point to inspect its call"), node("div", "plot-wrap", [chart]), text("div", "Green: training · Purple: validation · Dashed line: agreement. Points can overlap at identical values.", "chart-caption")]); | |
| const samples = state.index.summary.samples; | |
| const bins = Array.from({length:samples + 1}, (_, i) => ({value:i / samples, count:points.filter((p) => p.target === i / samples).length})); | |
| const max = Math.max(1, ...bins.map((b) => b.count)); | |
| const histogram = node("div", "histogram"); | |
| for (const bin of bins) { | |
| const fill = node("div", "fill"); fill.style.width = bin.count / max * 100 + "%"; | |
| histogram.append(node("div", "histogram-row", [text("span", number(bin.value)), node("div", "track", [fill]), text("strong", bin.count)])); | |
| } | |
| const notes = node("div", "method-note", [ | |
| text("strong", "Read these as sample outcomes."), | |
| text("p", "Each target averages " + samples + " terminal success outcomes. A target of 0 or 1 is not proof that the true probability is exactly 0 or 1."), | |
| text("p", "Predictions come from the prompted Q tool. No trained scalar value model is represented here. Only sampled calls contribute training targets."), | |
| text("p", "The MC continuation policy can use prompted Q tools; the unchanged historical estimator prompt describes tool-free continuations.") | |
| ]); | |
| $("calibration-view").replaceChildren(stats, node("div", "metrics-grid", [plot, node("section", "panel", [heading("MC target distribution", points.length + " calls"), histogram, notes])])); | |
| } | |
| function setTab(tab) { | |
| state.tab = tab; | |
| $("trajectory-view").hidden = tab !== "trajectory"; | |
| $("calibration-view").hidden = tab !== "calibration"; | |
| $("trajectory-tab").classList.toggle("active", tab === "trajectory"); | |
| $("calibration-tab").classList.toggle("active", tab === "calibration"); | |
| if (tab === "calibration") renderCalibration(); | |
| } | |
| async function initialize() { | |
| try { | |
| $("error").hidden = true; | |
| const response = await fetch("data/index.json", {cache:"no-store"}); | |
| if (!response.ok) throw new Error("Snapshot request returned " + response.status); | |
| state.index = await response.json(); | |
| state.cache.clear(); | |
| const s = state.index.summary; | |
| $("collection-info").replaceChildren(text("div", s.episodes + " trajectories · " + s.labels + " labels"), text("small", "Qwen3.5-9B · " + s.samples + " MC continuations")); | |
| $("snapshot-badge").textContent = s.collection_complete ? "Collection complete" : "Snapshot · " + s.episodes + " / " + s.planned_episodes + " trajectories"; | |
| $("snapshot-badge").classList.toggle("live", !s.collection_complete); | |
| $("dataset-link").href = "https://huggingface.co/datasets/" + s.dataset_repo; | |
| $("footer").textContent = "Snapshot " + new Date(s.created_at).toLocaleString() + " · Predictions are prompted Q estimates. MC targets are empirical success fractions. Wikipedia excerpts are CC BY-SA 4.0."; | |
| renderList(); | |
| const hash = new URLSearchParams(location.hash.slice(1)); | |
| const id = state.index.episodes.some((e) => e.id === hash.get("episode")) ? hash.get("episode") : | |
| state.index.episodes.find((e) => e.labels)?.id || state.index.episodes[0]?.id; | |
| if (id) await loadEpisode(id, hash.has("query") ? Number(hash.get("query")) : null); | |
| else $("trajectory-view").replaceChildren(text("div", "No completed trajectories in this snapshot.", "empty")); | |
| } catch (error) { showError(error); } | |
| } | |
| $("trajectory-tab").addEventListener("click", () => setTab("trajectory")); | |
| $("calibration-tab").addEventListener("click", () => setTab("calibration")); | |
| $("refresh").addEventListener("click", initialize); | |
| for (const id of ["search", "split-filter", "outcome-filter"]) { | |
| $(id).addEventListener(id === "search" ? "input" : "change", () => { | |
| if (!state.index) return; | |
| renderList(); if (state.tab === "calibration") renderCalibration(); | |
| }); | |
| } | |
| initialize(); | |