Spaces:
Running
Running
finlongdocqa-viewer bot
FinLongDocQA viewer: Corpus (1456 markdown reports) + Eval (7527 Q) static Space
8415278 | /* FinLongDocQA viewer — vanilla JS, two tabs: Corpus (markdown reports) + Eval. */ | |
| (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, "<") | |
| .replace(/>/g, ">") | |
| .replace(/"/g, """); | |
| } | |
| var state = { | |
| mode: "corpus", | |
| corpus: [], | |
| corpusView: [], | |
| corpusIdx: 0, | |
| byDocId: {}, | |
| mdCache: {}, // doc_id -> [{num, md}] | |
| curPages: [], | |
| curPageIdx: 0, | |
| pendingPage: null, // page number to jump to once the report loads | |
| loadSeq: 0, // guards against out-of-order async renders | |
| questions: [], | |
| evalView: [], | |
| evalIdx: 0, | |
| }; | |
| /* ---------------- data loading ---------------- */ | |
| function parseJSONL(text) { | |
| var out = []; | |
| text.split("\n").forEach(function (ln) { | |
| ln = ln.trim(); | |
| if (ln) { try { out.push(JSON.parse(ln)); } catch (e) {} } | |
| }); | |
| return out; | |
| } | |
| function loadAll() { | |
| return Promise.all([ | |
| fetch("corpus_index.json").then(function (r) { return r.json(); }), | |
| fetch("eval.json").then(function (r) { return r.json(); }), | |
| ]).then(function (res) { | |
| state.corpus = res[0] || []; | |
| state.questions = res[1] || []; | |
| state.corpus.forEach(function (d) { state.byDocId[d.doc_id] = d; }); | |
| }); | |
| } | |
| /* ---------------- corpus tab ---------------- */ | |
| function populateCorpusFilters() { | |
| var years = {}; | |
| state.corpus.forEach(function (d) { if (d.year) years[d.year] = true; }); | |
| var yf = $("yearFilter"); | |
| Object.keys(years).sort().forEach(function (y) { | |
| var o = document.createElement("option"); o.value = y; o.textContent = y; yf.appendChild(o); | |
| }); | |
| } | |
| function filterCorpus() { | |
| var q = $("corpusFilter").value.trim().toLowerCase(); | |
| var yr = $("yearFilter").value; | |
| var hasQ = $("hasQFilter").value; | |
| state.corpusView = state.corpus.filter(function (d) { | |
| if (yr && d.year !== yr) return false; | |
| if (hasQ === "q" && !d.n_questions) return false; | |
| if (hasQ === "noq" && d.n_questions) return false; | |
| if (q && (d.company || "").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 = d.title + (d.n_questions ? " · " + d.n_questions + "q" : ""); | |
| sel.appendChild(o); | |
| }); | |
| sel.value = String(state.corpusIdx); | |
| } | |
| function splitPages(md) { | |
| var lines = md.split("\n"); | |
| var re = /^#\s*Page\s+(\d+)\s*$/; | |
| var pages = [], cur = null; | |
| for (var i = 0; i < lines.length; i++) { | |
| var m = lines[i].match(re); | |
| if (m) { cur = { num: parseInt(m[1], 10), lines: [] }; pages.push(cur); } | |
| else if (cur) { cur.lines.push(lines[i]); } | |
| } | |
| if (!pages.length) pages = [{ num: 1, lines: lines }]; | |
| return pages.map(function (p) { return { num: p.num, md: p.lines.join("\n") }; }); | |
| } | |
| function renderCorpus() { | |
| var meta = $("docMetaCard"); | |
| var render = $("mdRender"); | |
| var nav = $("pageNav"); | |
| if (!state.corpusView.length) { | |
| meta.innerHTML = '<div class="empty">No reports match the filter.</div>'; | |
| render.innerHTML = ""; nav.style.display = "none"; | |
| $("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 = | |
| '<span class="meta-pill">Company: <b>' + esc(d.company) + "</b></span>" + | |
| '<span class="meta-pill">Fiscal year: <b>' + esc(d.year) + "</b></span>" + | |
| '<span class="meta-pill">Cited by <b>' + (d.n_questions || 0) + "</b> question(s)</span>"; | |
| var link = '<a class="doc-link" href="' + esc(d.md_url) + '" target="_blank" rel="noopener">raw .md ↗</a>'; | |
| meta.innerHTML = "<h2>" + esc(d.title) + " — Annual Report</h2>" + | |
| '<div class="meta-grid">' + pills + link + "</div>"; | |
| var seq = ++state.loadSeq; | |
| nav.style.display = "none"; | |
| render.className = "md-render loading"; | |
| render.textContent = "Loading report…"; | |
| var cached = state.mdCache[d.doc_id]; | |
| var got = cached ? Promise.resolve(cached) | |
| : fetch(d.md_url).then(function (r) { | |
| if (!r.ok) throw new Error("HTTP " + r.status); | |
| return r.text(); | |
| }).then(function (t) { var p = splitPages(t); state.mdCache[d.doc_id] = p; return p; }); | |
| got.then(function (pages) { | |
| if (seq !== state.loadSeq) return; // a newer selection superseded us | |
| state.curPages = pages; | |
| var idx = 0; | |
| if (state.pendingPage != null) { | |
| for (var i = 0; i < pages.length; i++) { if (pages[i].num === state.pendingPage) { idx = i; break; } } | |
| state.pendingPage = null; | |
| } | |
| state.curPageIdx = idx; | |
| rebuildPageSelect(); | |
| nav.style.display = "flex"; | |
| renderPage(); | |
| }).catch(function (err) { | |
| if (seq !== state.loadSeq) return; | |
| render.className = "md-render"; | |
| render.innerHTML = '<div class="empty">Failed to load report: ' + esc(err.message) + | |
| '<br><a href="' + esc(d.md_url) + '" target="_blank" rel="noopener">open raw markdown ↗</a></div>'; | |
| }); | |
| } | |
| function rebuildPageSelect() { | |
| var sel = $("pageSelect"); | |
| sel.innerHTML = ""; | |
| state.curPages.forEach(function (p, i) { | |
| var o = document.createElement("option"); | |
| o.value = String(i); | |
| o.textContent = "Page " + p.num; | |
| sel.appendChild(o); | |
| }); | |
| } | |
| function renderPage() { | |
| var render = $("mdRender"); | |
| var p = state.curPages[state.curPageIdx]; | |
| render.className = "md-render"; | |
| render.innerHTML = window.marked ? window.marked.parse(p.md) : esc(p.md); | |
| render.scrollTop = 0; | |
| $("pageCounter").textContent = "Page " + p.num + " (" + (state.curPageIdx + 1) + " / " + state.curPages.length + ")"; | |
| $("pageSelect").value = String(state.curPageIdx); | |
| $("pagePrevBtn").disabled = state.curPageIdx <= 0; | |
| $("pageNextBtn").disabled = state.curPageIdx >= state.curPages.length - 1; | |
| } | |
| function selectDocById(docId, pageNum) { | |
| $("corpusFilter").value = ""; | |
| $("yearFilter").value = ""; | |
| $("hasQFilter").value = ""; | |
| state.corpusView = state.corpus.slice(); | |
| var idx = 0; | |
| for (var i = 0; i < state.corpusView.length; i++) { | |
| if (state.corpusView[i].doc_id === docId) { idx = i; break; } | |
| } | |
| state.corpusIdx = idx; | |
| state.pendingPage = (pageNum != null) ? pageNum : null; | |
| rebuildDocSelect(); | |
| renderCorpus(); | |
| } | |
| /* ---------------- eval tab ---------------- */ | |
| function filterEval() { | |
| var q = $("evalSearch").value.trim().toLowerCase(); | |
| var typ = $("typeFilter").value; | |
| state.evalView = state.questions.filter(function (x) { | |
| if (typ && x.type !== typ) return false; | |
| if (q) { | |
| var hay = [x.question, x.company, x.id, x.title].join(" ").toLowerCase(); | |
| if (hay.indexOf(q) === -1) return false; | |
| } | |
| return true; | |
| }); | |
| 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); | |
| var q = (x.question || "").replace(/\s+/g, " "); | |
| if (q.length > 62) q = q.slice(0, 62) + "…"; | |
| o.textContent = "[" + x.title + "] " + q; | |
| 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 search.</div>'; | |
| $("evalCounter").textContent = "0 / 0"; | |
| return; | |
| } | |
| var x = state.evalView[state.evalIdx]; | |
| $("qSelect").value = String(state.evalIdx); | |
| $("evalCounter").textContent = (state.evalIdx + 1) + " / " + state.evalView.length; | |
| var badges = | |
| '<span class="badge id">#' + esc(x.id) + "</span>" + | |
| '<span class="badge type-' + esc(x.type) + '">' + esc(x.type) + "</span>" + | |
| '<span class="badge company" data-doc-id="' + esc(x.doc_id) + '" title="Open report in Corpus tab">' + | |
| esc(x.title) + " ↗</span>"; | |
| var pages = x.page_numbers || []; | |
| var pagesHtml = pages.length | |
| ? '<div class="evidence-pages">' + pages.map(function (n) { | |
| return '<span class="page-chip" data-doc-id="' + esc(x.doc_id) + '" data-page="' + n + | |
| '">📄 Page ' + esc(n) + "</span>"; | |
| }).join("") + "</div>" | |
| : '<span style="color:var(--muted)">none</span>'; | |
| var ansHtml = (x.answer === null || x.answer === undefined) ? "—" : esc(x.answer); | |
| card.innerHTML = | |
| '<div class="badges">' + badges + "</div>" + | |
| "<h2>" + esc(x.question) + "</h2>" + | |
| '<div class="answer-banner"><div class="lbl">Ground-truth answer</div><div class="val">' + | |
| ansHtml + "</div></div>" + | |
| field("Evidence pages", pagesHtml) + | |
| field("Reasoning trace (thoughts)", | |
| '<div class="thoughts-text">' + esc(x.thoughts || "—") + "</div>") + | |
| field("Python code", | |
| '<div class="code-block">' + esc(x.python_code || "—") + "</div>"); | |
| // company badge -> open report | |
| Array.prototype.forEach.call(card.querySelectorAll(".badge.company[data-doc-id]"), function (el) { | |
| el.addEventListener("click", function () { | |
| setMode("corpus"); | |
| selectDocById(this.getAttribute("data-doc-id")); | |
| }); | |
| }); | |
| // evidence page chip -> open report at that page | |
| Array.prototype.forEach.call(card.querySelectorAll(".page-chip[data-doc-id]"), function (el) { | |
| el.addEventListener("click", function () { | |
| setMode("corpus"); | |
| selectDocById(this.getAttribute("data-doc-id"), parseInt(this.getAttribute("data-page"), 10)); | |
| }); | |
| }); | |
| } | |
| /* ---------------- 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"; | |
| } | |
| /* ---------------- wiring ---------------- */ | |
| function wire() { | |
| $("modeCorpusBtn").addEventListener("click", function () { setMode("corpus"); }); | |
| $("modeEvalBtn").addEventListener("click", function () { setMode("eval"); }); | |
| // corpus | |
| $("corpusFilter").addEventListener("input", filterCorpus); | |
| $("yearFilter").addEventListener("change", filterCorpus); | |
| $("hasQFilter").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(); } | |
| }); | |
| // page nav | |
| $("pagePrevBtn").addEventListener("click", function () { | |
| if (state.curPageIdx > 0) { state.curPageIdx--; renderPage(); } | |
| }); | |
| $("pageNextBtn").addEventListener("click", function () { | |
| if (state.curPageIdx < state.curPages.length - 1) { state.curPageIdx++; renderPage(); } | |
| }); | |
| $("pageSelect").addEventListener("change", function () { | |
| state.curPageIdx = parseInt(this.value, 10) || 0; renderPage(); | |
| }); | |
| // eval | |
| $("evalSearchBtn").addEventListener("click", filterEval); | |
| $("evalSearch").addEventListener("keydown", function (e) { if (e.key === "Enter") filterEval(); }); | |
| $("evalClearBtn").addEventListener("click", function () { | |
| $("evalSearch").value = ""; $("typeFilter").value = ""; filterEval(); | |
| }); | |
| $("typeFilter").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(); } | |
| }); | |
| // keyboard arrows (report nav in corpus, question nav in eval) | |
| 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 $("evalPrevBtn").click(); | |
| } else if (e.key === "ArrowRight") { | |
| if (state.mode === "corpus") $("corpusNextBtn").click(); else $("evalNextBtn").click(); | |
| } | |
| }); | |
| } | |
| /* ---------------- boot ---------------- */ | |
| loadAll().then(function () { | |
| $("loading").style.display = "none"; | |
| $("sidebarFooter").innerHTML = | |
| state.corpus.length + " reports · " + state.questions.length + " questions<br>" + | |
| '<a href="https://github.com/AI-Application-and-Integration-Lab/FinLongDocQA" target="_blank" rel="noopener">GitHub</a> · ' + | |
| '<a href="https://huggingface.co/datasets/Amian/FinLongDocQA" target="_blank" rel="noopener">HF dataset</a> · ' + | |
| '<a href="https://arxiv.org/abs/2604.03664" target="_blank" rel="noopener">arXiv</a>'; | |
| populateCorpusFilters(); | |
| wire(); | |
| setMode("corpus"); | |
| filterCorpus(); | |
| filterEval(); | |
| }).catch(function (err) { | |
| $("loading").textContent = "Failed to load data: " + err; | |
| }); | |
| })(); | |