mebench-viewer / viewer.js
timchen0618's picture
Add MEBench viewer (Corpus + Eval tabs; train/test/single_test splits)
22dd326 verified
Raw
History Blame Contribute Delete
15.6 kB
/* MEBench viewer — vanilla JS. Two tabs: Corpus (shared entity docs) + Eval
(per-split questions with a train/test/single_test selector). */
(function () {
"use strict";
var $ = function (id) { return document.getElementById(id); };
function esc(s) {
if (s === null || s === undefined) return "";
return String(s)
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function truncate(s, n) {
s = String(s || "");
return s.length > n ? s.slice(0, n - 1) + "…" : s;
}
var state = {
splits: [], // manifest rows
splitName: null,
mode: "corpus",
corpus: [], // shared entity documents
corpusView: [],
corpusIdx: 0,
docByTitle: {}, // title -> index into state.corpus
topicCounts: {}, // topic -> #corpus docs
questions: [], // current split's questions
evalView: [],
evalIdx: 0,
};
/* ---------------- boot / loading ---------------- */
function boot() {
return Promise.all([
fetch("sets.json").then(function (r) { return r.json(); }),
fetch("corpus.json").then(function (r) { return r.json(); }),
]).then(function (res) {
state.splits = res[0] || [];
state.corpus = res[1] || [];
state.docByTitle = {};
state.topicCounts = {};
state.corpus.forEach(function (d, i) {
state.docByTitle[d.title] = i;
(d.topics || []).forEach(function (t) {
state.topicCounts[t] = (state.topicCounts[t] || 0) + 1;
});
});
populateCorpusTopicFilter();
populateSplitSelect();
// eval split loaded by loadSplit below
return loadSplit(state.splits[0].split);
});
}
function currentSplit() {
for (var i = 0; i < state.splits.length; i++) {
if (state.splits[i].split === state.splitName) return state.splits[i];
}
return null;
}
function loadSplit(name) {
state.splitName = name;
$("splitSelect").value = name;
var m = currentSplit();
return fetch(m.eval_file).then(function (r) { return r.json(); }).then(function (qs) {
state.questions = qs || [];
state.evalIdx = 0;
populateEvalFilters();
$("evalSearch").value = "";
filterEval();
renderFooter();
});
}
/* ---------------- corpus tab ---------------- */
function populateCorpusTopicFilter() {
var el = $("corpusTopicFilter");
el.innerHTML = '<option value="">All topics</option>';
Object.keys(state.topicCounts).sort().forEach(function (t) {
var o = document.createElement("option");
o.value = t; o.textContent = t + " (" + state.topicCounts[t] + ")";
el.appendChild(o);
});
}
function filterCorpus() {
var q = $("corpusFilter").value.trim().toLowerCase();
var topic = $("corpusTopicFilter").value;
state.corpusView = state.corpus.filter(function (d) {
if (topic && (d.topics || []).indexOf(topic) === -1) return false;
if (q && d.title.toLowerCase().indexOf(q) === -1) return false;
return true;
});
if (state.corpusIdx >= state.corpusView.length) state.corpusIdx = 0;
rebuildDocSelect();
renderCorpus();
}
function rebuildDocSelect() {
var sel = $("docSelect");
sel.innerHTML = "";
state.corpusView.forEach(function (d, i) {
var o = document.createElement("option");
o.value = String(i);
o.textContent = truncate(d.title, 52);
sel.appendChild(o);
});
sel.value = String(state.corpusIdx);
}
function renderCorpus() {
var meta = $("docMetaCard");
var body = $("docContent");
if (!state.corpusView.length) {
meta.innerHTML = '<div class="empty">No documents match the filter.</div>';
body.textContent = "";
$("corpusCounter").textContent = "0 / 0";
return;
}
var d = state.corpusView[state.corpusIdx];
$("docSelect").value = String(state.corpusIdx);
$("corpusCounter").textContent = (state.corpusIdx + 1) + " / " + state.corpusView.length;
var pills = (d.topics || []).map(function (t) {
return '<span class="meta-pill">Topic: <b>' + esc(t) + "</b></span>";
});
pills.push('<span class="meta-pill"><b>' + Math.round(d.size / 1024 * 10) / 10 + "</b> KB</span>");
var link = d.wiki_url
? '<a class="doc-link" href="' + esc(d.wiki_url) + '" target="_blank" rel="noopener">Wikipedia ↗</a>'
: "";
meta.innerHTML = "<h2>" + esc(d.title) + "</h2>" +
'<div class="meta-grid">' + pills.join("") + link + "</div>";
renderDocBody(d, body);
}
function renderDocBody(d, body) {
if (typeof d.content === "string") { body.textContent = d.content; body.scrollTop = 0; return; }
body.textContent = "Loading document…";
var target = d;
fetch(d.file).then(function (r) { return r.text(); }).then(function (text) {
target.content = text;
if (state.corpusView[state.corpusIdx] === target) { body.textContent = text; body.scrollTop = 0; }
}).catch(function (err) {
body.textContent = "Failed to load document: " + err;
});
}
function selectDocByTitle(title) {
$("corpusFilter").value = "";
$("corpusTopicFilter").value = "";
state.corpusView = state.corpus.slice();
var idx = 0;
for (var i = 0; i < state.corpusView.length; i++) {
if (state.corpusView[i].title === title) { idx = i; break; }
}
state.corpusIdx = idx;
rebuildDocSelect();
renderCorpus();
}
function showTopicInCorpus(topic) {
$("corpusFilter").value = "";
$("corpusTopicFilter").value = topic;
state.corpusIdx = 0;
filterCorpus();
}
/* ---------------- eval tab ---------------- */
function populateSplitSelect() {
var sel = $("splitSelect");
sel.innerHTML = "";
state.splits.forEach(function (s) {
var o = document.createElement("option");
o.value = s.split;
o.textContent = s.split + " (" + s.n_questions + " Q)";
sel.appendChild(o);
});
}
function populateEvalFilters() {
var m = currentSplit() || {};
var tf = $("evalTopicFilter");
tf.innerHTML = '<option value="">All topics</option>';
(m.topics || []).forEach(function (t) {
var o = document.createElement("option"); o.value = t; o.textContent = t; tf.appendChild(o);
});
var yf = $("evalTypeFilter");
yf.innerHTML = '<option value="">All question types</option>';
Object.keys(m.types || {}).forEach(function (t) {
var o = document.createElement("option");
o.value = t; o.textContent = t + " (" + m.types[t] + ")";
yf.appendChild(o);
});
}
function formatAnswer(a) {
if (a === null || a === undefined) return '<span class="empty-answer">null</span>';
if (Array.isArray(a)) {
if (!a.length) return '<span class="empty-answer">∅ empty result</span>';
// train SQL result shape: [columns, rows]
if (a.length === 2 && Array.isArray(a[0]) && Array.isArray(a[1])) {
var cols = a[0], rows = a[1];
var flat = [];
rows.forEach(function (r) { (Array.isArray(r) ? r : [r]).forEach(function (c) { flat.push(c); }); });
var allEmpty = !rows.length || flat.every(function (c) { return c === null || c === undefined; });
if (allEmpty) return '<span class="empty-answer">∅ no result (SQL returned empty / null)</span>';
var head = "<tr>" + cols.map(function (c) { return "<th>" + esc(c) + "</th>"; }).join("") + "</tr>";
var trs = rows.map(function (r) {
var cells = (Array.isArray(r) ? r : [r]);
return "<tr>" + cells.map(function (c) {
return "<td>" + (c === null || c === undefined ? "—" : esc(c)) + "</td>";
}).join("") + "</tr>";
}).join("");
return "<table>" + head + trs + "</table>";
}
return "<ul>" + a.map(function (x) { return "<li>" + esc(x) + "</li>"; }).join("") + "</ul>";
}
var s = String(a).trim();
return s ? esc(s) : '<span class="empty-answer">(empty)</span>';
}
function filterEval() {
var q = $("evalSearch").value.trim().toLowerCase();
var topic = $("evalTopicFilter").value;
var typ = $("evalTypeFilter").value;
state.evalView = state.questions.filter(function (x) {
if (topic && x.topic !== topic) return false;
if (typ && x.type !== typ) return false;
if (q) {
var hay = [x.question, JSON.stringify(x.answer), x.entity || "", x.topic]
.join(" ").toLowerCase();
if (hay.indexOf(q) === -1) return false;
}
return true;
});
if (state.evalIdx >= state.evalView.length) state.evalIdx = 0;
rebuildQuestionSelect();
renderEval();
}
function rebuildQuestionSelect() {
var sel = $("questionSelect");
sel.innerHTML = "";
state.evalView.forEach(function (x, i) {
var o = document.createElement("option");
o.value = String(i);
o.textContent = "#" + x.qid + " · " + truncate(x.question, 44);
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 renderEval() {
var card = $("evalCard");
if (!state.evalView.length) {
card.innerHTML = '<div class="empty">No questions match the filter.</div>';
$("evalCounter").textContent = "0 / 0";
return;
}
var x = state.evalView[state.evalIdx];
$("questionSelect").value = String(state.evalIdx);
$("evalCounter").textContent = (state.evalIdx + 1) + " / " + state.evalView.length;
var catCls = "cat-" + (x.category || "").split(" ")[0];
var badges =
'<span class="badge id">#' + esc(x.qid) + "</span>" +
(x.category ? '<span class="badge ' + catCls + '">' + esc(x.category) + "</span>" : "") +
(x.type ? '<span class="badge">' + esc(x.type) + "</span>" : "") +
(x.topic ? '<span class="badge topic">' + esc(x.topic) + "</span>" : "") +
'<span class="badge">' + esc(state.splitName) + "</span>";
var fields = "";
if (x.entity) fields += field("Entity", esc(x.entity));
if (x.edge) fields += field("Relation (edge)", esc(x.edge));
if (x.properties) fields += field("Properties", esc(x.properties));
if (x.hops) fields += field("Hops", esc(x.hops));
if (x["class"]) fields += field("Class", esc(x["class"]));
if (x.sql) fields += field("Ground-truth SQL", '<code class="sql-code">' + esc(x.sql) + "</code>");
// supporting documents
var support;
if (x.supporting_titles && x.supporting_titles.length) {
support = '<div class="doc-chips">' + x.supporting_titles.map(function (t) {
var known = state.docByTitle.hasOwnProperty(t);
return '<a class="doclink' + (known ? "" : " missing") + '"' +
(known ? ' data-doc="' + esc(t) + '"' : "") + ">" + esc(t) + (known ? " ↗" : "") + "</a>";
}).join("") + "</div>";
} else if (x.topic_level) {
var n = state.topicCounts[x.topic] || 0;
support = '<div class="doc-chips"><button class="topic-btn" data-topic="' + esc(x.topic) + '">' +
"📚 All " + n + " documents in “" + esc(x.topic) + "” ↗</button></div>" +
'<div style="color:var(--muted);font-size:12px;margin-top:6px;">' +
"This question ranges over every member of the topic." + "</div>";
} else {
support = '<span style="color:var(--muted)">none</span>';
}
var docsSection = field("Supporting documents", support);
card.innerHTML =
'<div class="badges">' + badges + "</div>" +
"<h2>" + esc(x.question) + "</h2>" +
'<div class="answer-banner"><div class="lbl">Gold answer</div><div class="val">' +
formatAnswer(x.answer) + "</div></div>" +
docsSection + fields;
Array.prototype.forEach.call(card.querySelectorAll("a.doclink[data-doc]"), function (link) {
link.addEventListener("click", function () {
setMode("corpus");
selectDocByTitle(this.getAttribute("data-doc"));
});
});
var tb = card.querySelector("button.topic-btn[data-topic]");
if (tb) tb.addEventListener("click", function () {
setMode("corpus");
showTopicInCorpus(this.getAttribute("data-topic"));
});
}
/* ---------------- mode switching ---------------- */
function setMode(mode) {
state.mode = mode;
$("modeCorpusBtn").classList.toggle("active", mode === "corpus");
$("modeEvalBtn").classList.toggle("active", mode === "eval");
$("corpusControls").style.display = mode === "corpus" ? "" : "none";
$("evalControls").style.display = mode === "eval" ? "" : "none";
$("corpusView").style.display = mode === "corpus" ? "flex" : "none";
$("evalView").style.display = mode === "eval" ? "block" : "none";
}
function renderFooter() {
$("sidebarFooter").innerHTML =
state.corpus.length + " entity docs · split <b>" + esc(state.splitName) + "</b> (" +
(currentSplit() || {}).n_questions + " Q)<br>" +
'<a href="https://github.com/tl2309/MEBench" target="_blank" rel="noopener">GitHub</a> · ' +
'<a href="https://huggingface.co/datasets/Tim999999/MEBench" target="_blank" rel="noopener">HF dataset</a> · ' +
'<a href="https://arxiv.org/abs/2502.18993" target="_blank" rel="noopener">paper</a>';
}
/* ---------------- wiring ---------------- */
function wire() {
$("modeCorpusBtn").addEventListener("click", function () { setMode("corpus"); });
$("modeEvalBtn").addEventListener("click", function () { setMode("eval"); });
$("corpusFilter").addEventListener("input", filterCorpus);
$("corpusTopicFilter").addEventListener("change", filterCorpus);
$("docSelect").addEventListener("change", function () {
state.corpusIdx = parseInt(this.value, 10) || 0; renderCorpus();
});
$("corpusPrevBtn").addEventListener("click", function () {
if (state.corpusIdx > 0) { state.corpusIdx--; renderCorpus(); }
});
$("corpusNextBtn").addEventListener("click", function () {
if (state.corpusIdx < state.corpusView.length - 1) { state.corpusIdx++; renderCorpus(); }
});
$("splitSelect").addEventListener("change", function () { loadSplit(this.value); });
$("evalSearch").addEventListener("input", filterEval);
$("evalTopicFilter").addEventListener("change", filterEval);
$("evalTypeFilter").addEventListener("change", filterEval);
$("questionSelect").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(); }
});
document.addEventListener("keydown", function (e) {
var tag = (e.target.tagName || "").toLowerCase();
if (tag === "input" || tag === "select" || tag === "textarea") return;
if (e.key === "ArrowLeft") {
(state.mode === "corpus" ? $("corpusPrevBtn") : $("evalPrevBtn")).click();
} else if (e.key === "ArrowRight") {
(state.mode === "corpus" ? $("corpusNextBtn") : $("evalNextBtn")).click();
}
});
}
/* ---------------- boot ---------------- */
wire();
boot().then(function () {
$("loading").style.display = "none";
setMode("corpus");
filterCorpus();
}).catch(function (err) {
$("loading").textContent = "Failed to load data: " + err;
});
})();