Spaces:
Running
Running
| /* HERB viewer — corpus, complete eval set, six answerable-only runs, and comparison. */ | |
| (function () { | |
| "use strict"; | |
| var $ = function (id) { return document.getElementById(id); }; | |
| var HERB_BASE = "https://huggingface.co/datasets/Salesforce/HERB/resolve/main/products/"; | |
| var RECORD_CACHE_LIMIT = 8; | |
| var ARTIFACT_TYPES = [ | |
| { key: "slack", label: "Slack", icon: "💬" }, | |
| { key: "documents", label: "Documents", icon: "📄" }, | |
| { key: "meeting_transcripts", label: "Meeting transcripts", icon: "🎙" }, | |
| { key: "meeting_chats", label: "Meeting chats", icon: "💭" }, | |
| { key: "urls", label: "URLs", icon: "🔗" }, | |
| { key: "prs", label: "Pull requests", icon: "🔀" }, | |
| ]; | |
| var TYPE_LABEL = {}; ARTIFACT_TYPES.forEach(function (t) { TYPE_LABEL[t.key] = t; }); | |
| function esc(s) { | |
| if (s === null || s === undefined) return ""; | |
| return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """); | |
| } | |
| function truncate(s, n) { s = String(s || "").replace(/\s+/g, " ").trim(); return s.length > n ? s.slice(0, n) + "…" : s; } | |
| var state = { | |
| mode: "corpus", | |
| products: [], productByName: {}, | |
| employees: {}, customers: {}, | |
| curProduct: null, productCache: {}, | |
| curType: null, artifactsView: [], artifactIdx: 0, | |
| pendingArtifactId: null, loadSeq: 0, | |
| questions: [], evalView: [], evalIdx: 0, | |
| manifest: null, runBySlot: {}, runIndexes: {}, runIndexPromises: {}, | |
| runRecords: {}, runRecordOrder: [], runRecordPromises: {}, | |
| runSlot: null, runView: [], runIdx: 0, runLoadSeq: 0, runFilterSeq: 0, | |
| compareIndex: null, compareIndexPromise: null, | |
| compareRecords: {}, compareRecordOrder: [], compareRecordPromises: {}, | |
| compareView: [], compareIdx: 0, compareLoadSeq: 0, compareFilterSeq: 0, | |
| }; | |
| function cachedRecord(cache, order, key) { | |
| if (!Object.prototype.hasOwnProperty.call(cache, key)) return null; | |
| var index = order.indexOf(key); | |
| if (index >= 0) order.splice(index, 1); | |
| order.push(key); | |
| return cache[key]; | |
| } | |
| function storeRecord(cache, order, key, record) { | |
| cache[key] = record; | |
| var index = order.indexOf(key); | |
| if (index >= 0) order.splice(index, 1); | |
| order.push(key); | |
| while (order.length > RECORD_CACHE_LIMIT) delete cache[order.shift()]; | |
| } | |
| /* ---------------- id resolution ---------------- */ | |
| function resolveEid(eid) { | |
| var e = state.employees[eid]; | |
| return e ? e.name : eid; | |
| } | |
| function eidTitle(eid) { | |
| var e = state.employees[eid]; | |
| return e ? (e.role || "") + (e.org ? " · " + e.org : "") : ""; | |
| } | |
| // escape text, then turn eid_xxxx tokens into resolved name tags | |
| function escResolve(text) { | |
| var out = esc(text); | |
| out = out.replace(/eid_[0-9a-f]{6,}/g, function (m) { | |
| return '<span class="eid-tag" title="' + esc(m + " — " + eidTitle(m)) + '">' + esc(resolveEid(m)) + "</span>"; | |
| }); | |
| return out; | |
| } | |
| function normalizeQid(value) { | |
| return String(value || "").replace(/#a(\d+)$/, "_a$1"); | |
| } | |
| function rememberQid(value) { | |
| var qid = normalizeQid(value); | |
| if (qid) window.TrajectoryUI.setQid(qid); | |
| } | |
| function questionMeta(value) { | |
| return window.HerbQuestionMetaByQid[normalizeQid(value)] || {}; | |
| } | |
| function computationFilterMatches(value, filter) { | |
| if (!filter) return true; | |
| return Boolean(questionMeta(value).computation_comparison) === (filter === "yes"); | |
| } | |
| function computationBadge(record) { | |
| var meta = record.computation_comparison === undefined | |
| ? questionMeta(record.qid || record.gid) | |
| : record; | |
| var isComputation = Boolean(meta.computation_comparison); | |
| var label = isComputation ? "computation / comparison" : "other"; | |
| return '<span class="badge computation-' + (isComputation ? "yes" : "no") + | |
| '" title="HERB computation/comparison split">' + label + "</span>"; | |
| } | |
| function sharedIndex(records) { | |
| var qid = window.TrajectoryUI.getQid(); | |
| if (!qid) return -1; | |
| return records.findIndex(function (record) { | |
| return normalizeQid(record.qid || record.gid) === qid; | |
| }); | |
| } | |
| function displayGold(value) { | |
| var values = Array.isArray(value) ? value : [value]; | |
| return values.map(function (item) { | |
| return typeof item === "string" && /^eid_[0-9a-f]+$/.test(item) ? resolveEid(item) : item; | |
| }); | |
| } | |
| window.HerbDisplayGold = displayGold; | |
| /* ---------------- data loading ---------------- */ | |
| function loadAll() { | |
| return Promise.all([ | |
| fetch("products.json").then(function (r) { return r.json(); }), | |
| fetch("eval.json").then(function (r) { return r.json(); }), | |
| fetch("employees.json").then(function (r) { return r.json(); }), | |
| fetch("customers.json").then(function (r) { return r.json(); }), | |
| fetch("runs/manifest.json").then(function (r) { if (!r.ok) throw new Error("run manifest HTTP " + r.status); return r.json(); }), | |
| ]).then(function (res) { | |
| state.products = res[0] || []; | |
| state.questions = res[1] || []; | |
| state.employees = res[2] || {}; | |
| (res[3] || []).forEach(function (c) { state.customers[c.id] = c; }); | |
| state.manifest = res[4] || { runs: [] }; | |
| (state.manifest.runs || []).forEach(function (run) { state.runBySlot[run.slot] = run; }); | |
| state.products.forEach(function (p) { state.productByName[p.name] = p; }); | |
| window.HerbQuestionMetaByQid = {}; | |
| state.questions.forEach(function (question) { | |
| window.HerbQuestionMetaByQid[normalizeQid(question.gid)] = question; | |
| }); | |
| }); | |
| } | |
| function loadProduct(name) { | |
| if (state.productCache[name]) return Promise.resolve(state.productCache[name]); | |
| return fetch(HERB_BASE + name + ".json").then(function (r) { | |
| if (!r.ok) throw new Error("HTTP " + r.status); | |
| return r.json(); | |
| }).then(function (d) { | |
| var byType = {}, byId = {}; | |
| ARTIFACT_TYPES.forEach(function (t) { | |
| var items = d[t.key] || []; | |
| byType[t.key] = items; | |
| items.forEach(function (it) { if (it && it.id != null && !(it.id in byId)) byId[it.id] = { type: t.key, item: it }; }); | |
| }); | |
| var rec = { data: d, byType: byType, byId: byId }; | |
| state.productCache[name] = rec; | |
| return rec; | |
| }); | |
| } | |
| /* ---------------- corpus tab ---------------- */ | |
| function populateProductSelects() { | |
| var cp = $("corpusProduct"), ep = $("evalProduct"), rp = $("runProduct"), xp = $("compareProduct"); | |
| state.products.forEach(function (p) { | |
| var o = document.createElement("option"); o.value = p.name; | |
| o.textContent = p.name + " (" + p.n_artifacts + ")"; cp.appendChild(o); | |
| var o2 = document.createElement("option"); o2.value = p.name; o2.textContent = p.name; ep.appendChild(o2); | |
| var o3 = document.createElement("option"); o3.value = p.name; o3.textContent = p.name; rp.appendChild(o3); | |
| var o4 = document.createElement("option"); o4.value = p.name; o4.textContent = p.name; xp.appendChild(o4); | |
| }); | |
| ["person", "content", "company", "pr", "url"].forEach(function (typ) { | |
| var r = document.createElement("option"); r.value = typ; r.textContent = typ; $("runType").appendChild(r); | |
| var x = document.createElement("option"); x.value = typ; x.textContent = typ; $("compareType").appendChild(x); | |
| }); | |
| } | |
| function populateTypeSelect(rec) { | |
| var sel = $("artifactType"); | |
| sel.innerHTML = ""; | |
| ARTIFACT_TYPES.forEach(function (t) { | |
| var n = (rec.byType[t.key] || []).length; | |
| if (!n) return; | |
| var o = document.createElement("option"); o.value = t.key; | |
| o.textContent = t.icon + " " + t.label + " (" + n + ")"; | |
| sel.appendChild(o); | |
| }); | |
| } | |
| function onProductChange() { | |
| var name = $("corpusProduct").value; | |
| state.curProduct = name; | |
| var seq = ++state.loadSeq; | |
| $("artifactMeta").innerHTML = ""; | |
| $("artifactBody").className = "artifact-body loading"; | |
| $("artifactBody").textContent = "Loading " + name + "…"; | |
| loadProduct(name).then(function (rec) { | |
| if (seq !== state.loadSeq) return; | |
| populateTypeSelect(rec); | |
| // choose type: pending artifact's type, else current if still valid, else first | |
| var chosen = null; | |
| if (state.pendingArtifactId != null && rec.byId[state.pendingArtifactId]) { | |
| chosen = rec.byId[state.pendingArtifactId].type; | |
| } else if (state.curType && (rec.byType[state.curType] || []).length) { | |
| chosen = state.curType; | |
| } | |
| if (!chosen) chosen = $("artifactType").options.length ? $("artifactType").options[0].value : null; | |
| state.curType = chosen; | |
| $("artifactType").value = chosen || ""; | |
| $("corpusFilter").value = ""; | |
| filterArtifacts(); | |
| }).catch(function (err) { | |
| if (seq !== state.loadSeq) return; | |
| $("artifactBody").className = "artifact-body"; | |
| $("artifactBody").innerHTML = '<div class="empty">Failed to load product: ' + esc(err.message) + "</div>"; | |
| }); | |
| } | |
| function artifactTitle(type, it) { | |
| switch (type) { | |
| case "slack": | |
| var ch = (it.Channel && it.Channel.name) ? "#" + it.Channel.name + ": " : ""; | |
| var txt = (it.Message && it.Message.User && it.Message.User.text) || ""; | |
| return ch + truncate(txt, 60) || it.id; | |
| case "documents": return (it.type ? it.type + " — " : "") + it.id; | |
| case "meeting_transcripts": return (it.document_type ? it.document_type + " — " : "") + it.id; | |
| case "meeting_chats": return it.id; | |
| case "urls": return it.description ? truncate(it.description, 70) : (it.link || it.id); | |
| case "prs": return (it.number ? "#" + it.number + " " : "") + (it.title || it.id); | |
| default: return it.id; | |
| } | |
| } | |
| function filterArtifacts() { | |
| var rec = state.productCache[state.curProduct]; | |
| if (!rec || !state.curType) { state.artifactsView = []; } | |
| else { | |
| var q = $("corpusFilter").value.trim().toLowerCase(); | |
| var items = rec.byType[state.curType] || []; | |
| state.artifactsView = items.map(function (it) { return { item: it, title: artifactTitle(state.curType, it) }; }) | |
| .filter(function (a) { return !q || a.title.toLowerCase().indexOf(q) !== -1 || String(a.item.id).toLowerCase().indexOf(q) !== -1; }); | |
| } | |
| if (state.artifactIdx >= state.artifactsView.length) state.artifactIdx = 0; | |
| rebuildArtifactSelect(); | |
| renderArtifact(); | |
| } | |
| function rebuildArtifactSelect() { | |
| var sel = $("artifactSelect"); | |
| sel.innerHTML = ""; | |
| state.artifactsView.forEach(function (a, i) { | |
| var o = document.createElement("option"); o.value = String(i); o.textContent = a.title; sel.appendChild(o); | |
| }); | |
| sel.value = String(state.artifactIdx); | |
| } | |
| function kvRow(pairs) { | |
| var rows = pairs.filter(function (p) { return p[1] !== undefined && p[1] !== null && p[1] !== ""; }) | |
| .map(function (p) { return '<div class="k">' + esc(p[0]) + '</div><div class="v">' + p[1] + "</div>"; }).join(""); | |
| return rows ? '<div class="kv">' + rows + "</div>" : ""; | |
| } | |
| function renderSlackReply(rep) { | |
| var u = (rep.Message && rep.Message.User) || rep.User || rep; | |
| var who = u.userId ? resolveEid(u.userId) : (rep.sender ? resolveEid(rep.sender) : ""); | |
| var txt = u.text || rep.text || rep.message || ""; | |
| return '<div class="slack-msg slack-reply"><span class="who">' + esc(who) + "</span>" + | |
| (u.timestamp ? '<span class="ts">' + esc(u.timestamp) + "</span>" : "") + | |
| '<div class="txt">' + escResolve(txt) + "</div></div>"; | |
| } | |
| function renderArtifactBody(type, it) { | |
| if (type === "slack") { | |
| var u = (it.Message && it.Message.User) || {}; | |
| var who = u.userId ? resolveEid(u.userId) : ""; | |
| var reacts = (it.Message && it.Message.Reactions) || []; | |
| var html = '<div class="slack-msg"><span class="who">' + esc(who) + "</span>" + | |
| (u.timestamp ? '<span class="ts">' + esc(u.timestamp) + "</span>" : "") + | |
| '<div class="txt">' + escResolve(u.text) + "</div></div>"; | |
| if (reacts.length) html += '<div class="field-title">Reactions</div><div class="pre-text">' + esc(JSON.stringify(reacts)) + "</div>"; | |
| var replies = it.ThreadReplies || []; | |
| if (replies.length) { | |
| html += '<div class="field-title">Thread replies (' + replies.length + ")</div>"; | |
| html += replies.map(renderSlackReply).join(""); | |
| } | |
| return html; | |
| } | |
| if (type === "documents") { | |
| var kv = kvRow([["Type", esc(it.type)], ["Author", escResolve(it.author)], ["Date", esc(it.date)]]); | |
| var body = window.marked ? window.marked.parse(it.content || "") : esc(it.content); | |
| return kv + '<div class="field-title">Content</div><div class="md-render">' + body + "</div>"; | |
| } | |
| if (type === "meeting_transcripts") { | |
| var parts = (it.participants || []).map(function (e) { return '<span class="eid-tag" title="' + esc(e) + '">' + esc(resolveEid(e)) + "</span>"; }).join(", "); | |
| var kv2 = kvRow([["Meeting type", esc(it.document_type)], ["Date", esc(it.date)], ["Participants", parts]]); | |
| return kv2 + '<div class="field-title">Transcript</div><div class="pre-text">' + escResolve(it.transcript) + "</div>"; | |
| } | |
| if (type === "meeting_chats") { | |
| return '<div class="field-title">Chat</div><div class="pre-text">' + escResolve(it.text) + "</div>"; | |
| } | |
| if (type === "urls") { | |
| return kvRow([["Link", '<a href="' + esc(it.link) + '" target="_blank" rel="noopener">' + esc(it.link) + " ↗</a>"], | |
| ["Description", escResolve(it.description)]]); | |
| } | |
| if (type === "prs") { | |
| var kv3 = kvRow([ | |
| ["Number", esc(it.number)], | |
| ["State", '<span class="state-badge state-' + esc(it.state) + '">' + esc(it.state) + "</span>"], | |
| ["Merged", esc(it.merged)], ["Mergeable", esc(it.mergeable)], | |
| ["Author", esc(it.user && it.user.login)], ["Created", esc(it.created_at)], | |
| ["Link", it.link ? '<a href="' + esc(it.link) + '" target="_blank" rel="noopener">' + esc(it.link) + " ↗</a>" : ""], | |
| ]); | |
| var summary = it.summary ? '<div class="field-title">Summary</div><div>' + escResolve(it.summary) + "</div>" : ""; | |
| var reviews = ""; | |
| if (it.reviews) { | |
| var parsed = null; | |
| try { parsed = JSON.parse(String(it.reviews).replace(/'/g, '"')); } catch (e) { parsed = null; } | |
| if (parsed && parsed.length) { | |
| reviews = '<div class="field-title">Reviews (' + parsed.length + ")</div>" + parsed.map(function (r) { | |
| return '<div class="review-item"><span class="state-badge state-' + esc(r.state) + '">' + esc(r.state) + "</span> " + | |
| esc(r.user && r.user.login) + (r.submitted_at ? ' · <span style="color:var(--muted)">' + esc(r.submitted_at) + "</span>" : "") + | |
| (r.comment ? '<div style="margin-top:4px">' + escResolve(r.comment) + "</div>" : "") + "</div>"; | |
| }).join(""); | |
| } else { | |
| reviews = '<div class="field-title">Reviews</div><div class="pre-text">' + esc(it.reviews) + "</div>"; | |
| } | |
| } | |
| return kv3 + summary + reviews; | |
| } | |
| return '<div class="pre-text">' + esc(JSON.stringify(it, null, 2)) + "</div>"; | |
| } | |
| function renderArtifact() { | |
| var meta = $("artifactMeta"), body = $("artifactBody"); | |
| if (!state.artifactsView.length) { | |
| meta.innerHTML = '<div class="empty">No artifacts match.</div>'; | |
| body.className = "artifact-body"; body.innerHTML = ""; | |
| $("corpusCounter").textContent = "0 / 0"; | |
| return; | |
| } | |
| var a = state.artifactsView[state.artifactIdx]; | |
| var it = a.item, type = state.curType; | |
| $("artifactSelect").value = String(state.artifactIdx); | |
| $("corpusCounter").textContent = (state.artifactIdx + 1) + " / " + state.artifactsView.length; | |
| var tl = TYPE_LABEL[type]; | |
| var link = ""; | |
| if (it.link) link = '<a class="doc-link" href="' + esc(it.link) + '" target="_blank" rel="noopener">open link ↗</a>'; | |
| else if (it.document_link) link = '<a class="doc-link" href="' + esc(it.document_link) + '" target="_blank" rel="noopener">source ↗</a>'; | |
| meta.innerHTML = "<h2>" + esc(a.title) + "</h2>" + | |
| '<div class="meta-grid">' + | |
| '<span class="meta-pill">Product: <b>' + esc(state.curProduct) + "</b></span>" + | |
| '<span class="meta-pill type-pill ' + type + '">' + esc(tl.label) + "</span>" + | |
| '<span class="meta-pill mono">' + esc(it.id) + "</span>" + link + "</div>"; | |
| body.className = "artifact-body"; | |
| body.innerHTML = renderArtifactBody(type, it); | |
| body.scrollTop = 0; | |
| } | |
| function selectArtifact(product, artifactId) { | |
| $("corpusProduct").value = product; | |
| state.pendingArtifactId = artifactId; | |
| state.artifactIdx = 0; | |
| // onProductChange will honor pendingArtifactId to pick the type; then locate the item | |
| var seq = ++state.loadSeq; | |
| state.curProduct = product; | |
| $("artifactBody").className = "artifact-body loading"; | |
| $("artifactBody").textContent = "Loading " + product + "…"; | |
| loadProduct(product).then(function (rec) { | |
| if (seq !== state.loadSeq) return; | |
| populateTypeSelect(rec); | |
| var hit = rec.byId[artifactId]; | |
| state.curType = hit ? hit.type : ($("artifactType").options.length ? $("artifactType").options[0].value : null); | |
| $("artifactType").value = state.curType || ""; | |
| $("corpusFilter").value = ""; | |
| state.pendingArtifactId = null; | |
| // build view then locate index of the artifact | |
| var items = rec.byType[state.curType] || []; | |
| state.artifactsView = items.map(function (x) { return { item: x, title: artifactTitle(state.curType, x) }; }); | |
| var idx = 0; | |
| for (var i = 0; i < state.artifactsView.length; i++) { if (state.artifactsView[i].item.id === artifactId) { idx = i; break; } } | |
| state.artifactIdx = idx; | |
| rebuildArtifactSelect(); | |
| renderArtifact(); | |
| }); | |
| } | |
| /* ---------------- eval tab ---------------- */ | |
| function filterEval() { | |
| var q = $("evalSearch").value.trim().toLowerCase(); | |
| var prod = $("evalProduct").value; | |
| var kind = $("kindFilter").value; | |
| var typ = $("qTypeFilter").value; | |
| var computationFilter = $("evalComputationFilter").value; | |
| state.evalView = state.questions.filter(function (x) { | |
| if (prod && x.product !== prod) return false; | |
| if (kind && x.kind !== kind) return false; | |
| if (typ && x.type !== typ) return false; | |
| if (!computationFilterMatches(x.gid, computationFilter)) return false; | |
| if (q) { | |
| var hay = [x.question, x.gid, x.product, Array.isArray(x.ground_truth) ? x.ground_truth.join(" ") : x.ground_truth].join(" ").toLowerCase(); | |
| if (hay.indexOf(q) === -1) return false; | |
| } | |
| return true; | |
| }); | |
| var target = sharedIndex(state.evalView); | |
| if (target >= 0) state.evalIdx = target; | |
| else if (state.evalIdx >= state.evalView.length) state.evalIdx = 0; | |
| rebuildQSelect(); | |
| renderEval(); | |
| } | |
| function rebuildQSelect() { | |
| var sel = $("qSelect"); | |
| sel.innerHTML = ""; | |
| state.evalView.forEach(function (x, i) { | |
| var o = document.createElement("option"); o.value = String(i); | |
| o.textContent = "[" + x.product + (x.kind === "unanswerable" ? " ⊘" : "") + "] " + truncate(x.question, 58); | |
| sel.appendChild(o); | |
| }); | |
| sel.value = String(state.evalIdx); | |
| } | |
| function field(lbl, valHtml) { | |
| return '<div class="field"><div class="lbl">' + esc(lbl) + '</div><div class="val">' + valHtml + "</div></div>"; | |
| } | |
| function renderGroundTruth(gt) { | |
| if (Array.isArray(gt)) { | |
| return '<ul class="gt-list">' + gt.map(function (v) { return "<li>" + escResolve(v) + "</li>"; }).join("") + "</ul>"; | |
| } | |
| return escResolve(gt); | |
| } | |
| function renderEval() { | |
| var card = $("evalCard"); | |
| if (!state.evalView.length) { | |
| card.innerHTML = '<div class="empty">No questions match.</div>'; | |
| $("evalCounter").textContent = "0 / 0"; | |
| return; | |
| } | |
| var x = state.evalView[state.evalIdx]; | |
| rememberQid(x.gid); | |
| $("qSelect").value = String(state.evalIdx); | |
| $("evalCounter").textContent = (state.evalIdx + 1) + " / " + state.evalView.length; | |
| var badges = | |
| '<span class="badge id">' + esc(x.gid) + "</span>" + | |
| '<span class="badge product" data-product="' + esc(x.product) + '" title="Browse this product in the Corpus tab">' + esc(x.product) + " ↗</span>" + | |
| '<span class="badge kind-' + esc(x.kind) + '">' + esc(x.kind) + "</span>" + | |
| (x.type && x.type !== "unanswerable" ? '<span class="badge qtype">' + esc(x.type) + "</span>" : "") + | |
| computationBadge(x); | |
| var answerHtml; | |
| if (x.kind === "unanswerable") { | |
| answerHtml = '<div class="answer-banner unans"><div class="lbl">Unanswerable</div>' + | |
| '<div class="val">This question has no answer in the corpus (intentional distractor).</div></div>'; | |
| } else { | |
| answerHtml = window.TrajectoryUI.gold(displayGold(x.ground_truth), "Gold"); | |
| } | |
| var cites = x.citations || []; | |
| var citesHtml = cites.length | |
| ? '<div class="cite-chips">' + cites.map(function (c) { | |
| return '<span class="cite-chip" data-product="' + esc(x.product) + '" data-cid="' + esc(c) + | |
| '"><span class="ic">📎</span>' + esc(c) + "</span>"; | |
| }).join("") + "</div>" | |
| : '<span style="color:var(--muted)">none</span>'; | |
| card.innerHTML = | |
| '<div class="badges">' + badges + "</div>" + | |
| "<h2>" + esc(x.question) + "</h2>" + | |
| answerHtml + | |
| (x.kind === "unanswerable" ? "" : | |
| field("Evidence citations (" + cites.length + ")", citesHtml)); | |
| Array.prototype.forEach.call(card.querySelectorAll(".cite-chip[data-cid]"), function (el) { | |
| el.addEventListener("click", function () { | |
| setMode("corpus"); | |
| selectArtifact(this.getAttribute("data-product"), this.getAttribute("data-cid")); | |
| }); | |
| }); | |
| var pb = card.querySelector(".badge.product[data-product]"); | |
| if (pb) pb.addEventListener("click", function () { | |
| setMode("corpus"); | |
| $("corpusProduct").value = this.getAttribute("data-product"); | |
| state.curType = null; onProductChange(); | |
| }); | |
| } | |
| /* ---------------- full-run + compare tabs ---------------- */ | |
| function buildTabButtons() { | |
| var tabs = [ | |
| { mode: "corpus", label: "📚 Corpus" }, | |
| { mode: "eval", label: "❓ Eval" }, | |
| ]; | |
| (state.manifest.runs || []).forEach(function (run) { | |
| tabs.push({ mode: run.slot, label: run.label }); | |
| }); | |
| tabs.push({ mode: "compare", label: "⚖ Compare" }); | |
| $("viewToggle").innerHTML = tabs.map(function (tab) { | |
| return '<button type="button" data-mode="' + esc(tab.mode) + '">' + esc(tab.label) + "</button>"; | |
| }).join(""); | |
| } | |
| function metricCard(label, value, detail, cls) { | |
| return '<div class="metric-card"><div class="metric-label">' + esc(label) + | |
| '</div><div class="metric-value ' + esc(cls || "") + '">' + esc(value) + | |
| '</div><div class="metric-detail">' + esc(detail) + "</div></div>"; | |
| } | |
| function runStatus(record) { | |
| if (!record.answered) return { key: "missing", label: "missing / unanswered" }; | |
| if (record.failure && /judge/.test(record.failure)) { | |
| return { key: "failure", label: "judge failure" }; | |
| } | |
| if (record.correct) return { key: "correct", label: "perfect" }; | |
| if (record.score > 0) return { key: "partial", label: "partial credit" }; | |
| return { key: "incorrect", label: "incorrect" }; | |
| } | |
| function citationChips(product, citations) { | |
| if (!citations || !citations.length) return '<span class="muted">none</span>'; | |
| return '<div class="cite-chips">' + citations.map(function (cid) { | |
| return '<span class="cite-chip" data-product="' + esc(product) + '" data-cid="' + esc(cid) + | |
| '"><span class="ic">📎</span>' + esc(cid) + "</span>"; | |
| }).join("") + "</div>"; | |
| } | |
| function wireCitationClicks(container) { | |
| Array.prototype.forEach.call(container.querySelectorAll(".cite-chip[data-cid]"), function (el) { | |
| el.addEventListener("click", function () { | |
| setMode("corpus"); | |
| selectArtifact(this.getAttribute("data-product"), this.getAttribute("data-cid")); | |
| }); | |
| }); | |
| Array.prototype.forEach.call(container.querySelectorAll(".badge.product[data-product]"), function (el) { | |
| el.addEventListener("click", function () { | |
| setMode("corpus"); | |
| $("corpusProduct").value = this.getAttribute("data-product"); | |
| state.curType = null; onProductChange(); | |
| }); | |
| }); | |
| } | |
| function questionBadges(record, status) { | |
| return '<div class="badges"><span class="badge id">' + esc(record.gid || record.qid) + "</span>" + | |
| '<span class="badge product" data-product="' + esc(record.product) + | |
| '" title="Browse this product in Corpus">' + esc(record.product) + " ↗</span>" + | |
| '<span class="badge qtype">' + esc(record.type) + "</span>" + | |
| computationBadge(record) + | |
| (status ? '<span class="badge status-' + esc(status.key) + '">' + esc(status.label) + "</span>" : "") + | |
| "</div>"; | |
| } | |
| function answerPanel(label, value, cls) { | |
| var html = value === null || value === undefined || value === "" | |
| ? '<span class="muted">none</span>' | |
| : (Array.isArray(value) ? renderGroundTruth(value) : escResolve(value)); | |
| return '<div class="answer-panel ' + esc(cls || "") + '"><div class="lbl">' + esc(label) + | |
| '</div><div class="val">' + html + "</div></div>"; | |
| } | |
| function runSummaryHtml(run) { | |
| return '<div class="scope-banner">Scope: 815 answerable HERB questions only. ' + | |
| "Each run uses its canonical evaluator; missing or unanswered questions count as zero.</div>" + | |
| '<div class="metric-grid">' + | |
| metricCard(run.score_label || "Score", run.score_pct.toFixed(2) + "%", | |
| run.score_detail || (run.correct + " / " + run.scope), "status-correct") + | |
| metricCard("Coverage", run.coverage_pct.toFixed(2) + "%", run.answered + " / " + run.scope + " answered", "") + | |
| metricCard("Missing", String(run.scope - run.answered), "explicit unanswered records", run.scope === run.answered ? "" : "status-missing") + | |
| "</div>"; | |
| } | |
| function ensureRunIndex(slot) { | |
| if (state.runIndexes[slot]) return Promise.resolve(state.runIndexes[slot]); | |
| if (state.runIndexPromises[slot]) return state.runIndexPromises[slot]; | |
| var request = fetch("runs/" + encodeURIComponent(slot) + "/index.json").then(function (r) { | |
| if (!r.ok) throw new Error("run index HTTP " + r.status); | |
| return r.json(); | |
| }).then(function (index) { | |
| state.runIndexes[slot] = index; | |
| delete state.runIndexPromises[slot]; | |
| return index; | |
| }, function (error) { | |
| delete state.runIndexPromises[slot]; | |
| throw error; | |
| }); | |
| state.runIndexPromises[slot] = request; | |
| return request; | |
| } | |
| function ensureRunRecord(slot, qid) { | |
| var key = slot + "/" + qid; | |
| var cached = cachedRecord(state.runRecords, state.runRecordOrder, key); | |
| if (cached) return Promise.resolve(cached); | |
| if (state.runRecordPromises[key]) return state.runRecordPromises[key]; | |
| var request = fetch("runs/" + encodeURIComponent(slot) + "/records/" + encodeURIComponent(qid) + ".json").then(function (r) { | |
| if (!r.ok) throw new Error("run record HTTP " + r.status); | |
| return r.json(); | |
| }).then(function (record) { | |
| delete state.runRecordPromises[key]; | |
| storeRecord(state.runRecords, state.runRecordOrder, key, record); | |
| return record; | |
| }, function (error) { | |
| delete state.runRecordPromises[key]; | |
| throw error; | |
| }); | |
| state.runRecordPromises[key] = request; | |
| return request; | |
| } | |
| function rebuildRunSelect() { | |
| var sel = $("runQSelect"); | |
| sel.innerHTML = ""; | |
| state.runView.forEach(function (item, i) { | |
| var option = document.createElement("option"); | |
| option.value = String(i); | |
| option.textContent = "[" + item.product + "] " + truncate(item.question, 58); | |
| sel.appendChild(option); | |
| }); | |
| sel.value = String(state.runIdx); | |
| } | |
| function filterRun() { | |
| var filterSeq = ++state.runFilterSeq; | |
| state.runLoadSeq++; | |
| var slot = state.runSlot; | |
| if (!slot) return; | |
| state.runView = []; | |
| $("runSummary").innerHTML = state.runBySlot[slot] | |
| ? runSummaryHtml(state.runBySlot[slot]) | |
| : ""; | |
| $("runQSelect").innerHTML = ""; | |
| $("runCounter").textContent = "0 / 0"; | |
| $("runCard").innerHTML = '<div class="empty">Loading run index…</div>'; | |
| ensureRunIndex(slot).then(function (index) { | |
| if (filterSeq !== state.runFilterSeq || state.mode !== slot) return; | |
| var q = $("runSearch").value.trim().toLowerCase(); | |
| var product = $("runProduct").value; | |
| var typ = $("runType").value; | |
| var status = $("runStatus").value; | |
| var computationFilter = $("runComputationFilter").value; | |
| state.runView = index.items.filter(function (item) { | |
| if (product && item.product !== product) return false; | |
| if (typ && item.type !== typ) return false; | |
| if (status === "correct" && !item.correct) return false; | |
| if (status === "partial" && !(item.answered && item.score > 0 && item.score < 1)) return false; | |
| if (status === "incorrect" && (!item.answered || item.correct || item.score > 0 || (item.failure && /judge/.test(item.failure)))) return false; | |
| if (status === "failure" && !(item.answered && item.failure && /judge/.test(item.failure))) return false; | |
| if (status === "missing" && item.answered) return false; | |
| if (!computationFilterMatches(item.qid, computationFilter)) return false; | |
| if (q) { | |
| var hay = [item.qid, item.gid, item.product, item.question, item.prediction, item.extracted_answer].join(" ").toLowerCase(); | |
| if (hay.indexOf(q) === -1) return false; | |
| } | |
| return true; | |
| }); | |
| var target = sharedIndex(state.runView); | |
| if (target >= 0) state.runIdx = target; | |
| else if (state.runIdx >= state.runView.length) state.runIdx = 0; | |
| rebuildRunSelect(); | |
| renderRun(); | |
| }).catch(function (err) { | |
| if (filterSeq === state.runFilterSeq && state.mode === slot) { | |
| $("runCard").innerHTML = '<div class="empty">Failed to load run: ' + esc(err.message) + "</div>"; | |
| } | |
| }); | |
| } | |
| function renderRun() { | |
| var seq = ++state.runLoadSeq; | |
| var run = state.runBySlot[state.runSlot]; | |
| $("runSummary").innerHTML = run ? runSummaryHtml(run) : ""; | |
| if (!state.runView.length) { | |
| $("runCard").innerHTML = '<div class="empty">No run questions match.</div>'; | |
| $("runCounter").textContent = "0 / 0"; | |
| return; | |
| } | |
| var item = state.runView[state.runIdx]; | |
| $("runQSelect").value = String(state.runIdx); | |
| $("runCounter").textContent = (state.runIdx + 1) + " / " + state.runView.length; | |
| $("runCard").innerHTML = '<div class="empty">Loading ' + esc(item.qid) + "…</div>"; | |
| var slot = state.runSlot; | |
| ensureRunRecord(slot, item.qid).then(function (record) { | |
| if (seq !== state.runLoadSeq || slot !== state.runSlot || state.mode !== slot) return; | |
| var status = runStatus(record); | |
| rememberQid(record.qid); | |
| var stop = record.stop_reason || record.finish_reason || (record.finish_reasons || []).slice(-1)[0] || "—"; | |
| var events = record.events || []; | |
| var eventHtml = window.TrajectoryUI.render(events, "Agent trajectory"); | |
| var citations = citationChips(record.product, record.citations); | |
| $("runCard").innerHTML = | |
| questionBadges(record, status) + | |
| "<h2>" + esc(record.question) + "</h2>" + | |
| window.TrajectoryUI.gold(displayGold(record.gold), "Gold") + | |
| (/^e2e_/.test(slot) | |
| ? window.TrajectoryUI.supporting( | |
| "Evidence citations", | |
| citations || '<span class="muted">none</span>', | |
| (record.citations || []).length | |
| ) | |
| : field("Evidence citations (" + (record.citations || []).length + ")", citations)) + | |
| answerPanel("Prediction", record.prediction, "prediction") + | |
| answerPanel("Extracted judge answer", record.extracted_answer, "") + | |
| '<div class="run-meta">' + | |
| '<span class="meta-pill">stop: <b>' + esc(stop) + "</b></span>" + | |
| '<span class="meta-pill">failure: <b>' + esc(record.failure || "none") + "</b></span>" + | |
| '<span class="meta-pill">confidence: <b>' + esc(record.confidence == null ? "—" : record.confidence) + "</b></span>" + | |
| (typeof record.score === "number" | |
| ? '<span class="meta-pill">canonical score: <b>' + (record.score * 100).toFixed(2) + "%</b></span>" | |
| : "") + | |
| '<span class="meta-pill">turns: <b>' + esc(record.turns == null ? "—" : record.turns) + "</b></span>" + | |
| '<span class="meta-pill">tokens: <b>' + esc((record.tokens.total_tokens || 0).toLocaleString()) + "</b></span>" + | |
| '<span class="meta-pill">tools: <b>' + esc(JSON.stringify(record.tool_counts || {})) + "</b></span>" + | |
| "</div>" + | |
| answerPanel("Judge text", record.judge_text, "") + | |
| eventHtml; | |
| wireCitationClicks($("runCard")); | |
| window.TrajectoryUI.bind($("runCard")); | |
| }).catch(function (err) { | |
| if (seq === state.runLoadSeq && slot === state.runSlot && state.mode === slot) { | |
| $("runCard").innerHTML = '<div class="empty">Failed to load record: ' + esc(err.message) + "</div>"; | |
| } | |
| }); | |
| } | |
| function ensureCompareIndex() { | |
| if (state.compareIndex) return Promise.resolve(state.compareIndex); | |
| if (state.compareIndexPromise) return state.compareIndexPromise; | |
| var request = fetch("compare/index.json").then(function (r) { | |
| if (!r.ok) throw new Error("compare index HTTP " + r.status); | |
| return r.json(); | |
| }).then(function (index) { | |
| state.compareIndex = index; | |
| state.compareIndexPromise = null; | |
| return index; | |
| }, function (error) { | |
| state.compareIndexPromise = null; | |
| throw error; | |
| }); | |
| state.compareIndexPromise = request; | |
| return request; | |
| } | |
| function ensureCompareRecord(qid) { | |
| var cached = cachedRecord(state.compareRecords, state.compareRecordOrder, qid); | |
| if (cached) return Promise.resolve(cached); | |
| if (state.compareRecordPromises[qid]) return state.compareRecordPromises[qid]; | |
| var request = fetch("compare/records/" + encodeURIComponent(qid) + ".json").then(function (r) { | |
| if (!r.ok) throw new Error("compare record HTTP " + r.status); | |
| return r.json(); | |
| }).then(function (record) { | |
| delete state.compareRecordPromises[qid]; | |
| storeRecord(state.compareRecords, state.compareRecordOrder, qid, record); | |
| return record; | |
| }, function (error) { | |
| delete state.compareRecordPromises[qid]; | |
| throw error; | |
| }); | |
| state.compareRecordPromises[qid] = request; | |
| return request; | |
| } | |
| function rebuildCompareSelect() { | |
| var sel = $("compareQSelect"); | |
| sel.innerHTML = ""; | |
| state.compareView.forEach(function (item, i) { | |
| var option = document.createElement("option"); | |
| option.value = String(i); | |
| option.textContent = "[" + item.product + "] " + truncate(item.question, 58); | |
| sel.appendChild(option); | |
| }); | |
| sel.value = String(state.compareIdx); | |
| } | |
| function compareSummaryHtml(index) { | |
| return '<div class="scope-banner">Compare scope: all runs joined to the same 815 answerable HERB questions. ' + | |
| "Missing or unanswered predictions receive zero.</div><div class=\"metric-grid\">" + | |
| index.runs.map(function (run) { | |
| return metricCard(run.label, run.score_pct.toFixed(2) + "%", (run.score_detail || run.correct + "/815") + " · " + | |
| run.coverage_pct.toFixed(2) + "% coverage (" + run.answered + " answered)", | |
| run.slot === "e2e_combined" ? "status-correct" : ""); | |
| }).join("") + "</div>"; | |
| } | |
| function filterCompare() { | |
| var filterSeq = ++state.compareFilterSeq; | |
| state.compareLoadSeq++; | |
| state.compareView = []; | |
| $("compareQSelect").innerHTML = ""; | |
| $("compareCounter").textContent = "0 / 0"; | |
| $("compareQuestion").innerHTML = '<div class="empty">Loading comparison index…</div>'; | |
| $("compareGrid").innerHTML = ""; | |
| ensureCompareIndex().then(function (index) { | |
| if (filterSeq !== state.compareFilterSeq || state.mode !== "compare") return; | |
| var q = $("compareSearch").value.trim().toLowerCase(); | |
| var product = $("compareProduct").value; | |
| var typ = $("compareType").value; | |
| var filter = $("compareFilter").value; | |
| var computationFilter = $("compareComputationFilter").value; | |
| state.compareView = index.items.filter(function (item) { | |
| if (product && item.product !== product) return false; | |
| if (typ && item.type !== typ) return false; | |
| if (filter === "disagreement" && !item.disagreement) return false; | |
| if (filter === "missing" && !item.any_missing) return false; | |
| if (filter === "combined-correct" && !item.only_e2e_combined_correct) return false; | |
| if (!computationFilterMatches(item.qid, computationFilter)) return false; | |
| if (q && [item.qid, item.gid, item.product, item.question].join(" ").toLowerCase().indexOf(q) === -1) return false; | |
| return true; | |
| }); | |
| var target = sharedIndex(state.compareView); | |
| if (target >= 0) state.compareIdx = target; | |
| else if (state.compareIdx >= state.compareView.length) state.compareIdx = 0; | |
| rebuildCompareSelect(); | |
| renderCompare(); | |
| }).catch(function (err) { | |
| if (filterSeq === state.compareFilterSeq && state.mode === "compare") { | |
| $("compareQuestion").innerHTML = '<div class="empty">Failed to load compare index: ' + esc(err.message) + "</div>"; | |
| } | |
| }); | |
| } | |
| function renderCompare() { | |
| var seq = ++state.compareLoadSeq; | |
| if (state.compareIndex) $("compareSummary").innerHTML = compareSummaryHtml(state.compareIndex); | |
| if (!state.compareView.length) { | |
| $("compareQuestion").innerHTML = '<div class="empty">No comparisons match.</div>'; | |
| $("compareGrid").innerHTML = ""; | |
| $("compareCounter").textContent = "0 / 0"; | |
| return; | |
| } | |
| var item = state.compareView[state.compareIdx]; | |
| $("compareQSelect").value = String(state.compareIdx); | |
| $("compareCounter").textContent = (state.compareIdx + 1) + " / " + state.compareView.length; | |
| $("compareQuestion").innerHTML = '<div class="empty">Loading ' + esc(item.qid) + "…</div>"; | |
| $("compareGrid").innerHTML = ""; | |
| ensureCompareRecord(item.qid).then(function (record) { | |
| if (seq !== state.compareLoadSeq || state.mode !== "compare") return; | |
| rememberQid(record.qid); | |
| $("compareQuestion").innerHTML = | |
| questionBadges(record, null) + "<h2>" + esc(record.question) + "</h2>" + | |
| window.TrajectoryUI.gold(displayGold(record.gold), "Gold") + | |
| field("Evidence citations (" + (record.citations || []).length + ")", citationChips(record.product, record.citations)); | |
| wireCitationClicks($("compareQuestion")); | |
| $("compareGrid").innerHTML = state.compareIndex.runs.map(function (run) { | |
| var result = record.runs[run.slot]; | |
| var status = runStatus(result); | |
| return '<article class="compare-card ' + esc(status.key) + '">' + | |
| '<div class="compare-head"><h3>' + esc(run.label) + '</h3><span class="badge status-' + | |
| esc(status.key) + '">' + esc(status.label) + "</span></div>" + | |
| '<div class="prediction-text">' + (result.prediction ? escResolve(result.prediction) : '<span class="muted">No prediction</span>') + "</div>" + | |
| '<details class="event-details"><summary>Judge + run metadata</summary><pre>' + | |
| esc(JSON.stringify({ | |
| extracted_answer: result.extracted_answer, | |
| canonical_score: result.score, | |
| score_kind: result.score_kind, | |
| judge_text: result.judge_text, | |
| confidence: result.confidence, | |
| failure: result.failure, | |
| stop_reason: result.stop_reason || result.finish_reason, | |
| turns: result.turns, | |
| tokens: result.tokens, | |
| tool_counts: result.tool_counts, | |
| }, null, 2)) + "</pre></details></article>"; | |
| }).join(""); | |
| }).catch(function (err) { | |
| if (seq === state.compareLoadSeq && state.mode === "compare") { | |
| $("compareQuestion").innerHTML = '<div class="empty">Failed to load comparison: ' + esc(err.message) + "</div>"; | |
| } | |
| }); | |
| } | |
| /* ---------------- mode switching ---------------- */ | |
| function setMode(mode) { | |
| if (mode !== "corpus" && mode !== "eval" && mode !== "compare" && !state.runBySlot[mode]) return; | |
| var previousMode = state.mode; | |
| var leftStructures = Boolean(window.__e2eStructuresJustClosed); | |
| window.__e2eStructuresJustClosed = false; | |
| state.mode = mode; | |
| Array.prototype.forEach.call($("viewToggle").querySelectorAll("button[data-mode]"), function (button) { | |
| button.classList.toggle("active", button.getAttribute("data-mode") === mode); | |
| }); | |
| $("corpusControls").style.display = mode === "corpus" ? "" : "none"; | |
| $("evalControls").style.display = mode === "eval" ? "" : "none"; | |
| $("runControls").style.display = state.runBySlot[mode] ? "" : "none"; | |
| $("compareControls").style.display = mode === "compare" ? "" : "none"; | |
| $("corpusView").style.display = mode === "corpus" ? "flex" : "none"; | |
| $("evalView").style.display = mode === "eval" ? "block" : "none"; | |
| $("runView").style.display = state.runBySlot[mode] ? "block" : "none"; | |
| $("compareView").style.display = mode === "compare" ? "block" : "none"; | |
| if (state.runBySlot[mode]) { | |
| if (previousMode !== mode || leftStructures) { | |
| $("runSearch").value = ""; | |
| $("runProduct").value = ""; | |
| $("runType").value = ""; | |
| $("runStatus").value = ""; | |
| $("runComputationFilter").value = ""; | |
| } | |
| state.runSlot = mode; | |
| filterRun(); | |
| } else if (mode === "compare") { | |
| if (previousMode !== "compare" || leftStructures) { | |
| $("compareSearch").value = ""; | |
| $("compareProduct").value = ""; | |
| $("compareType").value = ""; | |
| $("compareFilter").value = ""; | |
| $("compareComputationFilter").value = ""; | |
| } | |
| filterCompare(); | |
| } else if (mode === "eval") { | |
| if (previousMode !== "eval" || leftStructures) { | |
| $("evalSearch").value = ""; | |
| $("evalProduct").value = ""; | |
| $("kindFilter").value = ""; | |
| $("qTypeFilter").value = ""; | |
| $("evalComputationFilter").value = ""; | |
| } | |
| filterEval(); | |
| } | |
| } | |
| /* ---------------- wiring ---------------- */ | |
| function wire() { | |
| $("viewToggle").addEventListener("click", function (event) { | |
| var button = event.target.closest("button[data-mode]"); | |
| if (button) setMode(button.getAttribute("data-mode")); | |
| }); | |
| $("corpusProduct").addEventListener("change", function () { state.curType = null; onProductChange(); }); | |
| $("artifactType").addEventListener("change", function () { | |
| state.curType = this.value; state.artifactIdx = 0; $("corpusFilter").value = ""; filterArtifacts(); | |
| }); | |
| $("corpusFilter").addEventListener("input", function () { state.artifactIdx = 0; filterArtifacts(); }); | |
| $("artifactSelect").addEventListener("change", function () { state.artifactIdx = parseInt(this.value, 10) || 0; renderArtifact(); }); | |
| $("corpusPrevBtn").addEventListener("click", function () { if (state.artifactIdx > 0) { state.artifactIdx--; renderArtifact(); } }); | |
| $("corpusNextBtn").addEventListener("click", function () { if (state.artifactIdx < state.artifactsView.length - 1) { state.artifactIdx++; renderArtifact(); } }); | |
| $("evalSearchBtn").addEventListener("click", filterEval); | |
| $("evalSearch").addEventListener("keydown", function (e) { if (e.key === "Enter") filterEval(); }); | |
| $("evalClearBtn").addEventListener("click", function () { | |
| $("evalSearch").value = ""; $("evalProduct").value = ""; $("kindFilter").value = ""; $("qTypeFilter").value = ""; $("evalComputationFilter").value = ""; filterEval(); | |
| }); | |
| $("evalProduct").addEventListener("change", filterEval); | |
| $("kindFilter").addEventListener("change", filterEval); | |
| $("qTypeFilter").addEventListener("change", filterEval); | |
| $("evalComputationFilter").addEventListener("change", filterEval); | |
| $("qSelect").addEventListener("change", function () { state.evalIdx = parseInt(this.value, 10) || 0; renderEval(); }); | |
| $("evalPrevBtn").addEventListener("click", function () { if (state.evalIdx > 0) { state.evalIdx--; renderEval(); } }); | |
| $("evalNextBtn").addEventListener("click", function () { if (state.evalIdx < state.evalView.length - 1) { state.evalIdx++; renderEval(); } }); | |
| ["runSearch", "runProduct", "runType", "runStatus", "runComputationFilter"].forEach(function (id) { | |
| $(id).addEventListener(id === "runSearch" ? "input" : "change", function () { state.runIdx = 0; filterRun(); }); | |
| }); | |
| $("runQSelect").addEventListener("change", function () { state.runIdx = parseInt(this.value, 10) || 0; renderRun(); }); | |
| $("runPrevBtn").addEventListener("click", function () { if (state.runIdx > 0) { state.runIdx--; renderRun(); } }); | |
| $("runNextBtn").addEventListener("click", function () { if (state.runIdx < state.runView.length - 1) { state.runIdx++; renderRun(); } }); | |
| ["compareSearch", "compareProduct", "compareType", "compareFilter", "compareComputationFilter"].forEach(function (id) { | |
| $(id).addEventListener(id === "compareSearch" ? "input" : "change", function () { state.compareIdx = 0; filterCompare(); }); | |
| }); | |
| $("compareQSelect").addEventListener("change", function () { state.compareIdx = parseInt(this.value, 10) || 0; renderCompare(); }); | |
| $("comparePrevBtn").addEventListener("click", function () { if (state.compareIdx > 0) { state.compareIdx--; renderCompare(); } }); | |
| $("compareNextBtn").addEventListener("click", function () { if (state.compareIdx < state.compareView.length - 1) { state.compareIdx++; renderCompare(); } }); | |
| document.addEventListener("keydown", function (e) { | |
| var tag = (e.target.tagName || "").toLowerCase(); | |
| if (tag === "input" || tag === "select" || tag === "textarea") return; | |
| if (e.key === "ArrowLeft") { | |
| if (state.mode === "corpus") $("corpusPrevBtn").click(); | |
| else if (state.mode === "eval") $("evalPrevBtn").click(); | |
| else if (state.mode === "compare") $("comparePrevBtn").click(); | |
| else $("runPrevBtn").click(); | |
| } else if (e.key === "ArrowRight") { | |
| if (state.mode === "corpus") $("corpusNextBtn").click(); | |
| else if (state.mode === "eval") $("evalNextBtn").click(); | |
| else if (state.mode === "compare") $("compareNextBtn").click(); | |
| else $("runNextBtn").click(); | |
| } | |
| }); | |
| } | |
| /* ---------------- boot ---------------- */ | |
| loadAll().then(function () { | |
| $("loading").style.display = "none"; | |
| var tot = state.products.reduce(function (s, p) { return s + p.n_artifacts; }, 0); | |
| $("sidebarFooter").innerHTML = | |
| state.products.length + " products · " + tot.toLocaleString() + " artifacts · " + state.questions.length + " questions<br>" + | |
| '<a href="https://github.com/SalesforceAIResearch/HERB" target="_blank" rel="noopener">GitHub</a> · ' + | |
| '<a href="https://huggingface.co/datasets/Salesforce/HERB" target="_blank" rel="noopener">HF dataset</a> · ' + | |
| '<a href="https://arxiv.org/abs/2506.23139" target="_blank" rel="noopener">arXiv</a>'; | |
| populateProductSelects(); | |
| buildTabButtons(); | |
| wire(); | |
| setMode("corpus"); | |
| if (state.products.length) { $("corpusProduct").value = state.products[0].name; onProductChange(); } | |
| filterEval(); | |
| }).catch(function (err) { | |
| $("loading").textContent = "Failed to load data: " + err; | |
| }); | |
| })(); | |