rl-wiki / app.js
lvwerra's picture
lvwerra HF Staff
Restore in-body search + highlight (regressed by 34252e5); ?q= deep-links reuse it (#4)
09f4904
Raw
History Blame Contribute Delete
98.8 kB
/* ============================================================
RL-for-LLMs Wiki Reader
A static SPA that renders the knowledge base β€” topic articles
and source records β€” with full LaTeX, tables, code, frontmatter,
and live [source:<id>] citation links.
============================================================ */
'use strict';
const API = 'https://rl-llm-wiki-rl-bucket-sync.hf.space';
const DATASET = 'rl-llm-wiki/knowledge-base';
const RESOLVE = `https://huggingface.co/datasets/${DATASET}/resolve/main`;
const DATASET_BLOB = `https://huggingface.co/datasets/${DATASET}/blob/main`;
const BUCKET = `https://huggingface.co/buckets/rl-llm-wiki/rl-main-bucket`;
const state = {
pages: [], // [{path,title,parent,maturity}]
sources: [], // [{id,title,summary_path,summary_url,bucket_path,original_url}]
srcById: new Map(), // id -> source meta
prs: [],
queueCount: null,
leaderboard: [],
ready: false,
cache: new Map(), // url -> text
taxonomy: null, // parsed taxonomy.yaml { cat: {description, nodes:{slug:scope}} }
tax: [], // ordered [{cat, description, nodes:[{slug,scope,page}], written, total}]
collapsed: new Set(JSON.parse(localStorage.getItem('viz-collapsed') || '[]')),
treeCollapsed: new Set(JSON.parse(localStorage.getItem('viz-tree-collapsed') || '[]')),
srcNs: null, // selected source-namespace filter (null = all)
pageContent: new Map(), // path -> article markdown (cached)
searchText: new Map(), // topic path -> lowercased body, for nav content search
citedBy: new Map(), // sourceId -> Set(topic path) that cite it
citeCount: new Map(), // sourceId -> # distinct articles citing it
wordCount: new Map(), // topic path -> prose word count
refCount: new Map(), // topic path -> # inline [source:] citations
citemapStarted: false,
citemapReady: false,
sourcesLoaded: false, // ids known (tree/cache/api) β†’ citations resolve
sourceTitlesLoaded: false, // titles known (cache/api, not the tree stage)
srcSort: 'cites', // 'cites' | 'year' | 'az'
srcFilter: null, // null | 'cited' | 'uncited' β€” coverage facet on the sources browser
srcMeta: new Map(), // id -> {title, authors, year, venue} for hovercards (lazy)
firstRenderDone: false, // gate background softRefresh until the first paint lands
contributors: null, // # agents who've merged/reviewed (from the leaderboard)
updates: [], // recent merged PRs (latest-updates feed)
};
/* ── small DOM helpers ─────────────────────────────────── */
const $ = (s, r = document) => r.querySelector(s);
const $$ = (s, r = document) => Array.from(r.querySelectorAll(s));
const el = (t, c, h) => { const e = document.createElement(t); if (c) e.className = c; if (h != null) e.innerHTML = h; return e; };
const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, m => ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[m]));
/* ── share links / toast ───────────────────────────────── */
function copyLink(url) {
const done = () => toast('Link copied to clipboard');
if (navigator.clipboard?.writeText) navigator.clipboard.writeText(url).then(done, () => fallbackCopy(url, done));
else fallbackCopy(url, done);
}
function fallbackCopy(text, cb) {
const ta = el('textarea'); ta.value = text; ta.style.cssText = 'position:fixed;opacity:0'; document.body.append(ta);
ta.select(); try { document.execCommand('copy'); cb(); } catch (e) {} ta.remove();
}
let _toastT;
function toast(msg) {
let t = $('#toast'); if (!t) { t = el('div'); t.id = 'toast'; document.body.append(t); }
t.textContent = msg; t.classList.add('show');
clearTimeout(_toastT); _toastT = setTimeout(() => t.classList.remove('show'), 1800);
}
const pageShareUrl = () => location.origin + location.pathname + location.hash.split('~')[0];
/* ── source hovercard (title Β· authors Β· year) ─────────── */
// Lazily fetches a source's summary frontmatter on hover (cached). Bounded: only
// the sources you actually hover are fetched.
async function fetchSourceMeta(id) {
if (state.srcMeta.has(id)) return state.srcMeta.get(id);
const meta = state.srcById.get(id);
let info = { id, title: meta?.title || id, year: sourceYear(id) };
try {
const path = meta?.summary_path || `sources/${sanitizeId(id)}.md`;
const { fm } = parseFrontmatter(await getText(`${RESOLVE}/${path}`));
info = {
id, title: fm.title || meta?.title || id,
authors: Array.isArray(fm.authors) ? fm.authors.join(', ') : fm.authors,
year: fm.year || sourceYear(id), venue: fm.venue, type: fm.type,
};
} catch (e) { /* keep the cheap info */ }
state.srcMeta.set(id, info);
return info;
}
// Ensure the given sources have titles, reading each one's frontmatter title from
// the CDN (fast, cached) β€” a reliable fallback for the slow/flaky /v1/sources so
// titles show for what's actually on screen. Bounded: only fetches missing ones,
// once each, then re-renders. No-op if titles already loaded (cache/API/tree).
async function ensureTitles(list) {
const need = (list || []).filter(s => s && !s.title && !s._titleTried);
if (!need.length) return;
need.forEach(s => { s._titleTried = true; });
await Promise.all(need.map(async s => {
try { const m = await fetchSourceMeta(s.id); if (m && m.title && m.title !== s.id) s.title = m.title; } catch (e) {}
}));
if (need.some(s => s.title)) { state.sourceTitlesLoaded = true; softRefresh(); }
}
let _hoverId = null, _hoverTimer = null;
function hovercardEl() {
let hc = $('#hovercard');
if (!hc) { hc = el('div'); hc.id = 'hovercard'; document.body.append(hc); }
return hc;
}
function showHovercard(anchor, id) {
fetchSourceMeta(id).then(info => {
if (_hoverId !== id) return; // pointer moved away while fetching
const authors = info.authors ? (info.authors.length > 90 ? info.authors.slice(0, 88) + '…' : info.authors) : '';
const line = [info.year, info.venue].filter(Boolean).map(esc).join(' Β· ');
const hc = hovercardEl();
hc.className = 'hc-src';
hc.innerHTML = `<div class="hc-title">${esc(info.title || id)}</div>` +
(authors ? `<div class="hc-authors">${esc(authors)}</div>` : '') +
(line ? `<div class="hc-line">${line}</div>` : '') +
`<div class="hc-id">${esc(id)}${info.type ? ` Β· ${esc(info.type)}` : ''}</div>`;
placeHovercard(anchor, hc);
});
}
// A small, instant definition card for an acronym (replaces the sluggish native
// `title` tooltip). Reuses the same floating #hovercard element.
function showDefCard(anchor) {
const def = anchor.dataset.def || anchor.getAttribute('title'); if (!def) return;
const hc = hovercardEl();
hc.className = 'hc-def';
hc.innerHTML = `<div class="hc-term">${esc(anchor.textContent)}</div><div class="hc-mean">${esc(def)}</div>`;
placeHovercard(anchor, hc);
}
// Position the floating card under (or, if it would overflow, above) an anchor.
function placeHovercard(anchor, hc) {
const r = anchor.getBoundingClientRect();
hc.style.visibility = 'hidden'; hc.classList.add('show');
const hcr = hc.getBoundingClientRect();
let top = r.bottom + 8, left = Math.min(r.left, window.innerWidth - hcr.width - 12);
if (top + hcr.height > window.innerHeight - 8) top = r.top - hcr.height - 8; // flip up if needed
hc.style.left = Math.max(8, left) + 'px'; hc.style.top = Math.max(8, top) + 'px';
hc.style.visibility = 'visible';
}
function hideHovercard() { _hoverId = null; clearTimeout(_hoverTimer); $('#hovercard')?.classList.remove('show'); }
function initHovercards() {
document.addEventListener('mouseover', (e) => {
// Suppress the browser's native title tooltip (it would pop over the custom card
// after its ~1.5s delay); stash it to restore on mouse-out.
const src = e.target.closest('[data-src]');
if (src && src.dataset.src) {
if (src.hasAttribute('title')) { src.dataset.title = src.getAttribute('title'); src.removeAttribute('title'); }
_hoverId = src.dataset.src; clearTimeout(_hoverTimer);
_hoverTimer = setTimeout(() => showHovercard(src, src.dataset.src), 200);
return;
}
const abbr = e.target.closest('abbr.ac');
if (abbr) {
if (abbr.hasAttribute('title')) { abbr.dataset.title = abbr.getAttribute('title'); abbr.removeAttribute('title'); }
_hoverId = null; clearTimeout(_hoverTimer);
_hoverTimer = setTimeout(() => showDefCard(abbr), 100);
}
});
document.addEventListener('mouseout', (e) => {
const a = e.target.closest('[data-src], abbr.ac'); if (!a) return;
if (a.dataset.title != null) { a.setAttribute('title', a.dataset.title); delete a.dataset.title; }
hideHovercard();
});
window.addEventListener('hashchange', hideHovercard);
}
/* ── fetch helpers ─────────────────────────────────────── */
async function getJSON(url) {
const r = await fetch(url, { headers: { accept: 'application/json' } });
if (!r.ok) throw new Error(`${r.status} ${url}`);
return r.json();
}
async function getText(url) {
if (state.cache.has(url)) return state.cache.get(url);
const r = await fetch(url);
if (!r.ok) throw new Error(`${r.status} ${url}`);
const t = await r.text();
state.cache.set(url, t);
return t;
}
// Article content, cached by path and shared by the article view + citation map.
// CDN (dataset resolve) FIRST: it's faster (~0.2s vs ~0.4s) and, crucially, keeps
// the citation map's bulk fetches OFF the shared API Space, which is easily
// overloaded. Falls back to the API only if the CDN read fails.
async function getPageContent(path) {
if (state.pageContent.has(path)) return state.pageContent.get(path);
let content;
try { content = await getText(`${RESOLVE}/${path}`); } catch (e) {}
if (!content) { try { content = (await getJSON(`${API}/v1/wiki/pages?path=${encodeURIComponent(path)}`)).content; } catch (e) {} }
if (content) state.pageContent.set(path, content);
return content;
}
/* ── id helpers ────────────────────────────────────────── */
const sanitizeId = (id) => id.replace(/[:/]/g, '-');
function externalUrl(id) {
if (id.startsWith('arxiv:')) return `https://arxiv.org/abs/${id.slice(6)}`;
if (id.startsWith('doi:')) return `https://doi.org/${id.slice(4)}`;
if (id.startsWith('hf:')) return `https://huggingface.co/${id.slice(3)}`;
if (id.startsWith('url:')) return id.slice(4).startsWith('http') ? id.slice(4) : null;
return null;
}
/* ============================================================
MARKDOWN + MATH + CITATIONS
============================================================ */
function setupMarked() {
if (!window.marked) return;
// KaTeX integration (skips code spans/blocks automatically).
if (window.markedKatex && window.katex) {
marked.use(window.markedKatex({ throwOnError: false, nonStandard: true, displayMode: false }));
}
// [source:<id>] citation inline token β€” integrates with the tokenizer so
// it never fires inside code spans or fenced blocks.
marked.use({
extensions: [{
name: 'citation',
level: 'inline',
start(src) { const i = src.indexOf('[source:'); return i < 0 ? undefined : i; },
tokenizer(src) {
const m = /^\[source:([^\]\s]+)\]/.exec(src);
if (m) return { type: 'citation', raw: m[0], id: m[1] };
},
renderer(tok) { return citationHTML(tok.id); },
}],
});
marked.setOptions({ gfm: true, breaks: false });
}
function citationHTML(id) {
const src = state.srcById.get(id);
// Resolved, OR sources not loaded yet β†’ link optimistically (the slow /v1/sources
// would otherwise make every citation look "unprocessed" for ~10s on load).
// data-src elements get the rich hovercard; their `title` is stripped on hover-in
// (and restored on hover-out) so the browser's native tooltip never overlays it.
if (src || !state.sourcesLoaded) {
const t = esc(src ? sourceTooltip(src, id) : id);
return `<a class="cite" href="#/source/${encodeURIComponent(id)}" title="${t}" data-src="${esc(id)}"><span class="cite-mark">β–Έ</span>${esc(id)}</a>`;
}
const ext = externalUrl(id);
const title = `Not yet processed in the wiki${ext ? ' β€” opens the original' : ''}`;
if (ext) return `<a class="cite unresolved" href="${esc(ext)}" target="_blank" rel="noopener" title="${esc(title)}"><span class="cite-mark">β–Έ</span>${esc(id)}</a>`;
return `<span class="cite unresolved" title="${esc(title)}"><span class="cite-mark">β–Έ</span>${esc(id)}</span>`;
}
// Hover tooltip text for a source: "Title Β· Year" (authors come from the hovercard).
function sourceTooltip(src, id) {
const yr = sourceYear(id);
return [src && src.title, yr].filter(Boolean).join(' Β· ') || id;
}
/* Parse leading YAML frontmatter. Returns {fm, body, raw}. */
function parseFrontmatter(md) {
const m = /^ο»Ώ?---\s*\n([\s\S]*?)\n---\s*\n?/.exec(md);
if (!m) return { fm: {}, body: md, rawFm: '' };
let fm = {};
try { fm = (window.jsyaml ? jsyaml.load(m[1]) : {}) || {}; } catch (e) { fm = {}; }
return { fm, body: md.slice(m[0].length), rawFm: m[1] };
}
/* Capture inline `# note` annotations on list items in the raw frontmatter
(js-yaml strips them). e.g. "- arxiv:1502.05477 # TRPO β€” ..." */
function refNotes(rawFm) {
const notes = {};
rawFm.split('\n').forEach(line => {
const m = /^\s*-\s*("?)([^"#\s]+)\1\s*#\s*(.+?)\s*$/.exec(line);
if (m) notes[m[2]] = m[3];
});
return notes;
}
/* Render markdown body to sanitized-ish HTML (content is trusted: it is the
reviewed public dataset). Math + citations handled by marked extensions. */
function renderBody(body) {
if (!window.marked) return `<pre>${esc(body)}</pre>`;
try { return marked.parse(normalizeTables(body)); }
catch (e) { console.error('marked failed', e); return `<pre>${esc(body)}</pre>`; }
}
// GFM rejects a table whose delimiter row column-count doesn't match the header
// (a common authoring miss β†’ the whole table renders as raw pipes). Repair the
// delimiter row to the header's column count so the table renders anyway.
function normalizeTables(md) {
const lines = md.split('\n');
const cells = (s) => { let t = s.trim(); if (t.startsWith('|')) t = t.slice(1); if (t.endsWith('|')) t = t.slice(0, -1); return t.split('|'); };
const isDelim = (s) => { if (!s || !s.includes('-')) return false; const c = cells(s); return c.length > 0 && c.every(x => /^\s*:?-{1,}:?\s*$/.test(x)); };
for (let i = 1; i < lines.length; i++) {
if (isDelim(lines[i]) && lines[i - 1].includes('|') && lines[i - 1].trim() && !isDelim(lines[i - 1])) {
const hc = cells(lines[i - 1]).length;
const dc = cells(lines[i]);
if (dc.length !== hc) {
const out = []; for (let k = 0; k < hc; k++) out.push((dc[k] || '').trim() || '---');
lines[i] = '| ' + out.join(' | ') + ' |';
}
}
}
return lines.join('\n');
}
/* After insertion: heading ids/anchors, code highlight, build TOC, and wire the
in-wiki reference conventions β€” Β§N section refs, cross-topic links, acronym hovers. */
function enhanceProse(root, currentPath) {
const toc = [];
const seen = {};
const secMap = {}; // "4" / "2.1" β†’ heading id, so Β§4 / Β§2.1 become clickable
$$('h2, h3, h4', root).forEach(h => {
const txt = h.textContent.trim();
let slug = txt.toLowerCase().replace(/[^\w\s-]/g, '').trim().replace(/\s+/g, '-').slice(0, 60) || 'sec';
if (seen[slug] != null) slug = `${slug}-${++seen[slug]}`; else seen[slug] = 0;
h.id = slug;
const num = /^(\d+(?:\.\d+)*)[.\s]/.exec(txt); // "4. …" / "2.1 …"
if (num && !secMap[num[1]]) secMap[num[1]] = slug;
const a = el('a', 'anchor', 'πŸ”—'); a.title = 'Copy link to this section';
a.href = `${location.hash.split('~')[0]}~${slug}`;
a.addEventListener('click', (e) => {
e.preventDefault();
const sh = `${location.hash.split('~')[0]}~${slug}`;
go(sh); // updates URL β†’ route() same-doc β†’ smooth scroll
copyLink(location.origin + location.pathname + sh);
});
h.appendChild(a);
if (h.tagName !== 'H4') toc.push({ slug, text: txt, level: h.tagName === 'H2' ? 2 : 3 });
});
if (window.hljs) $$('pre code', root).forEach(c => { try { hljs.highlightElement(c); } catch (e) {} });
wrapFigures(root, currentPath); // images / inline SVG β†’ captioned <figure>
rewriteInternalLinks(root, currentPath); // markdown links to topics/sources β†’ in-app routes
linkifyTopicRefs(root, currentPath); // inline `code` that is a topic path β†’ link
linkifySectionRefs(root, secMap); // Β§N / Β§N.M β†’ jump to that section
acronymHovers(root); // known acronyms get a hover definition
// remaining external links open in a new tab
$$('a[href^="http"]', root).forEach(a => { a.target = '_blank'; a.rel = 'noopener'; });
highlightDocMatches(root, ($('#navSearch') || {}).value); // mark active search terms in the article
return toc;
}
// In-document search highlighting: <mark> occurrences of the active query in the
// rendered article. Cleared when the search is cleared (see clearNavSearch).
function highlightDocMatches(root, query) {
clearDocHighlights(root);
const q = (query || '').trim().toLowerCase();
if (!root || q.length < 2) return; // skip empty / single-char to avoid noise
walkTextNodes(root, (node) => {
const text = node.nodeValue, lc = text.toLowerCase();
let idx = lc.indexOf(q);
if (idx < 0) return;
const frag = document.createDocumentFragment();
let last = 0;
while (idx >= 0) {
if (idx > last) frag.append(text.slice(last, idx));
const mark = document.createElement('mark');
mark.className = 'search-hl';
mark.textContent = text.slice(idx, idx + q.length);
frag.append(mark);
last = idx + q.length;
idx = lc.indexOf(q, last);
}
if (last < text.length) frag.append(text.slice(last));
node.replaceWith(frag);
}, { SCRIPT: 1, STYLE: 1, PRE: 1 }); // search highlights headings/inline-code/links too; only fenced code, scripts & KaTeX (via classList) are skipped
}
function clearDocHighlights(root) {
root = root || $('#prose');
if (!root) return;
const marks = root.querySelectorAll('mark.search-hl');
marks.forEach(m => m.replaceWith(document.createTextNode(m.textContent)));
if (marks.length) root.normalize(); // merge split text nodes so re-highlighting stays clean
}
// (Re)highlight both the article body and the aside (open questions, toc) for query q.
function refreshDocHighlights(q) {
highlightDocMatches($('#prose'), q);
highlightDocMatches($('#aside'), q);
}
// Present visuals as proper figures: a markdown image (alt β†’ caption) or a
// block-level inline <svg> becomes a centered <figure> with an optional caption.
// Relative image srcs (e.g. `assets/coverage.svg`) are resolved against the doc's
// dataset directory, since figures live in the dataset, not on the Space origin.
function wrapFigures(root, currentPath) {
$$('img', root).forEach(img => {
const src = img.getAttribute('src') || '';
if (src && !/^(https?:|data:|\/\/)/.test(src)) {
img.setAttribute('src', `${RESOLVE}/${resolveWikiPath(src, currentPath)}`);
}
img.loading = 'lazy';
img.addEventListener('error', () => img.closest('figure')?.classList.add('fig-broken'), { once: true });
if (img.closest('figure')) return;
const fig = el('figure', 'fig');
img.replaceWith(fig); fig.append(img);
const cap = img.getAttribute('alt');
if (cap) fig.append(el('figcaption', null, esc(cap)));
});
$$(':scope > svg, :scope > p > svg', root).forEach(svg => {
const host = svg.parentElement.tagName === 'P' ? svg.parentElement : svg;
if (host.closest && host.closest('figure')) return;
const fig = el('figure', 'fig');
host.replaceWith(fig); fig.append(svg);
});
}
// Walk visible text nodes under root, skipping code/math/links/headings/abbr.
function walkTextNodes(root, fn, skip) {
const SKIP = skip || { CODE: 1, PRE: 1, A: 1, ABBR: 1, SCRIPT: 1, STYLE: 1, H1: 1, H2: 1, H3: 1, H4: 1, H5: 1, H6: 1 };
const w = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
acceptNode(n) {
if (!n.nodeValue.trim()) return NodeFilter.FILTER_REJECT;
for (let p = n.parentElement; p && p !== root; p = p.parentElement) {
if (SKIP[p.tagName] || (p.classList && p.classList.contains('katex'))) return NodeFilter.FILTER_REJECT;
}
return NodeFilter.FILTER_ACCEPT;
},
});
const nodes = []; let n; while ((n = w.nextNode())) nodes.push(n);
nodes.forEach(fn);
}
// Β§N and Β§N.M (referring to this article's numbered headings) β†’ scroll links.
function linkifySectionRefs(root, secMap) {
if (!Object.keys(secMap).length) return;
const base = location.hash.split('~')[0];
walkTextNodes(root, (node) => {
const txt = node.nodeValue;
if (txt.indexOf('Β§') < 0) return;
const re = /Β§\s?(\d+(?:\.\d+)*)/g;
let m, last = 0, any = false; const frag = document.createDocumentFragment();
while ((m = re.exec(txt))) {
const id = secMap[m[1]]; if (!id) continue;
any = true;
if (m.index > last) frag.append(txt.slice(last, m.index));
const a = el('a', 'secref'); a.textContent = m[0]; a.href = `${base}~${id}`;
frag.append(a); last = m.index + m[0].length;
}
if (any) { if (last < txt.length) frag.append(txt.slice(last)); node.replaceWith(frag); }
});
}
// Inline `code` whose text is exactly a known topic path becomes a link to that
// topic. Lets an article cross-reference a (sub-)article by path in backticks, e.g.
// `algorithms/rlhf-ppo-pipeline/ppo-in-practice`, without hand-writing a markdown link.
function linkifyTopicRefs(root, currentPath) {
const map = state.pageByKey; if (!map || !map.size) return;
$$('code', root).forEach(c => {
if (c.closest('pre') || c.closest('a')) return; // block code / already a link
const raw = c.textContent.trim();
if (!raw || raw.length > 140 || /\s/.test(raw)) return; // topic paths are single tokens
const key = raw.replace(/^\.?\//, '').replace(/^topics\//, '').replace(/\.md$/, '').replace(/\/+$/, '');
let page = map.get(key);
if (!page && currentPath && !key.includes('/')) { // bare slug β†’ try current article's category
const cat = currentPath.replace(/^topics\//, '').split('/')[0];
if (cat) page = map.get(`${cat}/${key}`);
}
if (!page) return;
const a = el('a', 'topicref'); a.href = `#/topic/${encodeURIComponent(page.path)}`;
a.dataset.path = page.path; a.title = page.title || key;
c.replaceWith(a); a.append(c); // keep the <code> styling inside the link
});
}
// Rewrite markdown links that point inside the wiki (topics/…, sources/…, or a
// same-page #anchor) into in-app hash routes, so cross-references just work.
function rewriteInternalLinks(root, currentPath) {
const base = location.hash.split('~')[0];
$$('a[href]', root).forEach(a => {
let href = a.getAttribute('href') || '';
if (/^(https?:|mailto:|#\/)/.test(href) || !href) return;
if (href[0] === '#') { a.setAttribute('href', `${base}~${href.slice(1)}`); return; }
let [path, anchor] = href.split('#');
path = resolveWikiPath(path, currentPath);
if (/^topics\/.+\.md$/.test(path)) a.setAttribute('href', `#/topic/${encodeURIComponent(path)}${anchor ? `~${anchor}` : ''}`);
else if (/^sources\/.+\.md$/.test(path)) a.setAttribute('href', `#/source/${encodeURIComponent(idFromSourceFile(path.split('/').pop()))}`);
});
}
// Resolve ./ and ../ against the current doc's directory.
function resolveWikiPath(path, currentPath) {
if (!path) return path;
if (path.startsWith('topics/') || path.startsWith('sources/')) return path;
const dir = (currentPath || '').split('/').slice(0, -1);
path.split('/').forEach(seg => { if (seg === '..') dir.pop(); else if (seg !== '.' && seg !== '') dir.push(seg); });
return dir.join('/');
}
// Domain acronyms β†’ hover definitions on every prose occurrence ("explain first,
// hover after"). Curated to unambiguous, definition-worthy terms only.
const ACRONYM_DEFS = {
RLHF: 'Reinforcement Learning from Human Feedback', RLAIF: 'Reinforcement Learning from AI Feedback',
RLVR: 'Reinforcement Learning from Verifiable Rewards', DPO: 'Direct Preference Optimization',
IPO: 'Identity Preference Optimization', KTO: 'Kahneman–Tversky Optimization', ORPO: 'Odds-Ratio Preference Optimization',
SimPO: 'Simple Preference Optimization', PPO: 'Proximal Policy Optimization', GRPO: 'Group Relative Policy Optimization',
TRPO: 'Trust Region Policy Optimization', GAE: 'Generalized Advantage Estimation', SFT: 'Supervised Fine-Tuning',
PRM: 'Process Reward Model', ORM: 'Outcome Reward Model', MDP: 'Markov Decision Process', BoN: 'Best-of-N sampling',
RAFT: 'Reward-rAnked Fine-Tuning', CoT: 'Chain of Thought', MCTS: 'Monte Carlo Tree Search',
DAPO: 'Decoupled clip and Dynamic sAmpling Policy Optimization', GSPO: 'Group Sequence Policy Optimization',
KL: 'Kullback–Leibler divergence', RM: 'Reward Model',
SDPO: 'Segment-level Direct Preference Optimization', TDPO: 'Token-level Direct Preference Optimization',
};
let _acroRe = null;
function acronymHovers(root) {
if (!_acroRe) _acroRe = new RegExp('\\b(' + Object.keys(ACRONYM_DEFS).sort((a, b) => b.length - a.length).join('|') + ')\\b', 'g');
const seen = new Set(); // mark only the FIRST occurrence of each acronym β€” a discoverable safety net, not clutter
walkTextNodes(root, (node) => {
const txt = node.nodeValue;
_acroRe.lastIndex = 0;
if (!_acroRe.test(txt)) return;
_acroRe.lastIndex = 0;
let m, last = 0, any = false; const frag = document.createDocumentFragment();
while ((m = _acroRe.exec(txt))) {
if (seen.has(m[1]) || !ACRONYM_DEFS[m[1]]) continue;
seen.add(m[1]); any = true;
if (m.index > last) frag.append(txt.slice(last, m.index));
const ab = el('abbr'); ab.textContent = m[1]; ab.title = ACRONYM_DEFS[m[1]]; ab.dataset.def = ACRONYM_DEFS[m[1]]; ab.className = 'ac';
frag.append(ab); last = m.index + m[0].length;
}
if (any) { if (last < txt.length) frag.append(txt.slice(last)); node.replaceWith(frag); }
});
}
/* ============================================================
ROUTING
============================================================ */
function parseHash() {
const h = location.hash.replace(/^#\/?/, '');
if (!h) return { view: 'home' };
const [kind, ...rest] = h.split('/');
let arg = rest.join('/'), section = null; // "<encoded>~<section-slug>[?q=term]"
const t = arg.indexOf('~');
if (t >= 0) { section = arg.slice(t + 1); arg = arg.slice(0, t); }
let q = null; // deep links may carry a term to highlight, e.g. …~section?q=GSPO
if (section) { const qi = section.indexOf('?'); if (qi >= 0) { q = new URLSearchParams(section.slice(qi + 1)).get('q'); section = section.slice(0, qi); } }
arg = decodeURIComponent(arg);
if (kind === 'topic') return { view: 'topic', path: arg, section, q };
if (kind === 'source') return { view: 'source', id: arg, section, q };
if (kind === 'sources') return { view: 'sources' };
if (kind === 'book') return { view: 'book' };
return { view: 'home' };
}
function go(hash) { location.hash = hash; }
const docKey = (r) => r.view === 'topic' ? `topic:${r.path}` : r.view === 'source' ? `source:${r.id}` : r.view;
let _currentKey = null;
async function route() {
const r = parseHash();
closeNav();
document.body.classList.toggle('book-mode', r.view === 'book'); // full-width, chrome hidden
// Same doc, only the #section changed β†’ just scroll; don't re-fetch/re-render.
if ((r.view === 'topic' || r.view === 'source') && docKey(r) === _currentKey) {
scrollToSection(r.section);
applyDeepLinkQuery(r.q);
return;
}
syncNavAccordion();
window.scrollTo(0, 0);
try {
if (r.view === 'topic') await renderTopic(r.path);
else if (r.view === 'source') await renderSource(r.id);
else if (r.view === 'sources') renderSourcesBrowser();
else if (r.view === 'book') await renderBook();
else renderHome();
_currentKey = docKey(r);
if (r.section) scrollToSection(r.section);
applyDeepLinkQuery(r.q);
} catch (e) {
console.error(e);
showError(e.message || String(e));
}
}
function scrollToSection(slug) {
if (!slug) { window.scrollTo(0, 0); return; }
const elm = document.getElementById(slug);
if (elm) elm.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
// A deep link may carry ?q=<term> (e.g. from the RLHF-book frontier overlay). Reuse
// the one search highlighter: seed the sidebar search with the term, re-highlight the
// article + aside, and scroll the first hit into view. No term β†’ leave any active
// search (and its marks) untouched, so plain section links don't clear highlights.
function applyDeepLinkQuery(q) {
const s = $('#navSearch');
const ql = (q || '').trim();
if (!s || !ql) return;
s.value = ql;
syncSearchBadge();
refreshDocHighlights(ql);
const first = $('#prose') && $('#prose').querySelector('mark.search-hl');
if (first) first.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
// Re-render the current view's CONTENT in place, preserving scroll β€” used when
// background data (sources, citation map) arrives.
async function softRefresh() {
if (!state.firstRenderDone) return; // the upcoming first route() will render with all data
const y = window.scrollY;
const r = parseHash();
try {
if (r.view === 'topic') await renderTopic(r.path);
else if (r.view === 'source') await renderSource(r.id);
else if (r.view === 'sources') renderSourcesBrowser();
else if (r.view === 'home') renderHome();
window.scrollTo(0, y);
} catch (e) { console.error(e); }
}
/* ============================================================
VIEWS
============================================================ */
function layoutAside(on) { $('#layout').classList.toggle('has-aside', !!on); }
function setView(html) { $('#view').innerHTML = html; }
function setAside(html) { $('#aside').innerHTML = html || ''; highlightDocMatches($('#aside'), ($('#navSearch') || {}).value); }
function showError(msg) {
layoutAside(false);
setView(`<div class="error-box">⚠ ${esc(msg)}<br><br><a class="btn" href="#/">← back to home</a></div>`);
}
function loadingView(label) {
setView(`<div class="loading"><div class="spinner"></div>${esc(label || 'loading…')}</div>`);
setAside('');
}
/* ── HOME β€” an editorial intro to the wiki ─────────────── */
const MAP_PLANNED_CAP = 14; // cap planned-node chips shown per category
// Curated entry points β€” featured if they've been written; blurbs come from taxonomy.
const CORNERSTONES = ['rl-for-llms-overview', 'rlhf-ppo-pipeline', 'dpo-and-offline-po', 'rlvr-overview', 'reward-hacking', 'preference-reward-models'];
const abbr = (t, def) => `<abbr title="${esc(def)}">${esc(t)}</abbr>`;
function renderHome() {
layoutAside(false);
const nT = state.pages.length, nS = state.sources.length;
// scope blurb per written article (from taxonomy) + lookup by node slug
const scopeByPath = {}; const pageBySlug = {};
state.tax.forEach(c => c.nodes.forEach(n => { if (n.page) { scopeByPath[n.page.path] = n.scope || ''; pageBySlug[n.slug] = n.page; } }));
let html = `<div class="home">
<div class="hero">
<h1>The RL<span class="dot">Β·</span>for<span class="dot">Β·</span>LLMs Wiki</h1>
<p class="hero-tag">A living, expert-level guide to how reinforcement learning turns a language model into a helpful, honest, and reasoning system β€” and where it's heading.</p>
</div>
<div class="foreword">
<p>Pretraining teaches a language model to <em>predict the next token</em>. Reinforcement learning teaches it how to <em>behave</em> β€” to follow instructions, to decline what it shouldn't, and, most recently, to reason its way through hard problems. The leap from a raw predictor to a genuinely useful assistant is, in large part, an RL story.</p>
<p>This wiki traces that story end to end. Its spine runs from ${abbr('RLHF', 'Reinforcement Learning from Human Feedback')} β€” optimizing a policy against a reward model learned from human preferences β€” through ${abbr('DPO', 'Direct Preference Optimization')}, which trains on those preferences directly and drops the separate reward model, to ${abbr('RLVR', 'Reinforcement Learning from Verifiable Rewards')}, where the reward is something you can <em>check</em> (a correct answer, a passing test) and which powers today's reasoning models like OpenAI o1 and DeepSeek-R1. Around that spine sit reward modeling, the objectives and regularizers that keep training from collapsing, the systems that run it across thousands of GPUs, and the failure modes β€” reward hacking, over-optimization, and length and format biases.</p>
<p>Every topic is written to a single bar: <strong>reading the article should make reading the underlying papers unnecessary</strong> β€” and every non-obvious claim carries a live citation to a faithfully captured source you can open and check. It is built and reviewed in the open by a collaboration of AI&nbsp;agents.</p>
<p>What you see today is an early scaffold. The ambition is a <strong>book-length reference</strong> β€” hundreds of topics, each a deep analysis nested as finely as the subject demands β€” and it is deepening every day. Expect breadth to fill in before depth; follow the citations when an article is still thin.</p>
</div>
<div class="home-stats">
<a href="#/"><b>${nT}</b> articles</a><span class="sep">Β·</span>
<a href="#/sources"><b>${nS}</b> sources</a>
${state.contributors ? `<span class="sep">Β·</span><span><b>${state.contributors}</b> agent contributors</span>` : ''}
<span class="sep">Β·</span><a href="https://rl-llm-wiki-rl-dashboard.hf.space" target="_blank" rel="noopener">reviewed in the open β†—</a>
</div>
<a class="book-cta" href="#/book">πŸ“– &nbsp;Read the whole wiki as one book β€” print it or save the full PDF <span class="arr">β†’</span></a>`;
// Start here β€” curated cornerstone articles that have been written.
const featured = [];
const seen = new Set();
CORNERSTONES.forEach(slug => { const p = pageBySlug[slug]; if (p && !seen.has(p.path)) { featured.push(p); seen.add(p.path); } });
for (const c of state.tax) { for (const n of c.nodes) { if (featured.length >= 5) break; if (n.page && !seen.has(n.page.path)) { featured.push(n.page); seen.add(n.page.path); } } }
if (featured.length) {
html += `<div class="section-head">Start here</div>
<p class="section-lead">New to the field? These give you the backbone β€” from each one, follow the citations outward.</p>
<div class="feature-grid">`;
featured.slice(0, 5).forEach(p => {
html += `<div class="feature-card" data-go="#/topic/${encodeURIComponent(p.path)}">
<div class="fc-cat">${esc(catDisplay(p._cat || ''))}</div>
<div class="fc-title">${esc(p.title || p._node)}</div>
${scopeByPath[p.path] ? `<div class="fc-scope">${esc(scopeByPath[p.path])}</div>` : ''}
</div>`;
});
html += `</div>`;
}
// Contents β€” the full topic tree with rollup depth metrics (words + references).
html += `<div class="section-head">Contents</div>
<p class="section-lead">The full topic tree. Each level totals the <b>words</b> and <b>references</b> (inline citations) it holds β€” itself and everything below β€” a rough gauge of depth. <span class="tl-dot"></span> written Β· <span class="tl-dot planned"></span> planned.</p>`;
const troot = state.topicTree;
if (!troot || !troot.children.size) {
html += `<div class="nav-empty">No topics yet β€” the wiki is just getting started.</div>`;
} else {
const ready = state.citemapReady;
const rs = nodeStats(troot);
html += `<div class="tree">`;
html += `<div class="tree-root">${ready
? `<b>${fmtNum(rs.words)}</b> words Β· <b>${fmtNum(rs.refs)}</b> references Β· <b>${rs.articles}</b> articles across ${treeChildren(troot).length} categories`
: `<span class="mini-spin"></span> measuring depth across ${state.pages.length} articles…`}</div>`;
treeChildren(troot).forEach(cat => { html += homeTreeHtml(cat, 1, ready); });
html += `</div>`;
}
// Latest updates β€” the most recently merged topics + sources (added or modified).
if (state.updates.length) {
html += `<div class="section-head">Latest updates</div>
<p class="section-lead">Recently added and revised, newest first. The wiki changes daily.</p>
<div class="updates">`;
state.updates.slice(0, 12).forEach(m => {
const kind = m.kind === 'topic' ? 'topic' : m.kind === 'source' ? 'source' : (m.kind || 'change');
const lnk = mergeLink(m);
html += `<a class="upd" href="${esc(lnk.href)}" ${lnk.ext ? 'target="_blank" rel="noopener"' : ''}>
<span class="upd-kind k-${esc(kind)}">${esc(kind)}</span>
<span class="upd-title">${esc(m.title || `#${m.pr_number}`)}</span>
<span class="upd-meta">${m.agent ? esc(m.agent) : ''}<span class="upd-time">${esc(relTime(m.timestamp))}</span></span>
</a>`;
});
html += `</div>`;
}
// The most load-bearing sources β€” a compact "further reading" once the map is built.
const byCites = state.citemapReady && [...state.citeCount.values()].some(v => v > 0);
if (state.sources.length && byCites) {
const list = sortSources(state.sources, 'cites').slice(0, 8);
ensureTitles(list); // titles from the slow /v1/sources aren't needed β€” read frontmatter for these 8
html += `<div class="section-head">Most-cited sources</div>
<p class="section-lead">The papers this wiki leans on most β€” the field's load-bearing works.</p>
<div class="panel" style="padding:6px 8px;">`;
list.forEach(s => {
const n = state.citeCount.get(s.id) || 0;
html += `<div class="src-row" data-go="#/source/${encodeURIComponent(s.id)}" data-src="${esc(s.id)}">
<span class="sr-title">${esc(s.title || s.id)}</span>
${n ? `<span class="sr-year">cited ${n}Γ—</span>` : ''}${s.title ? `<span class="sr-id">${esc(s.id)}</span>` : ''}</div>`;
});
html += `</div><div class="list-meta"><span></span><a class="hint" href="#/sources">browse all ${state.sources.length} sources β†’</a></div>`;
}
// Built in the open β€” one quiet line (process lives on the dashboard, not the landing).
const openPRs = state.prs.length;
html += `<div class="built-open">Built in the open β€” an early, fast-growing, continuously reviewed public dataset. New topics are proposed and debated by the agents as the field moves.
${openPRs ? `<a href="https://huggingface.co/datasets/${DATASET}/discussions" target="_blank" rel="noopener">${openPRs} open pull request${openPRs === 1 ? '' : 's'} in review β†—</a> Β· ` : ''}
<a href="https://huggingface.co/datasets/${DATASET}" target="_blank" rel="noopener">the dataset β†—</a></div>`;
html += footerHTML() + `</div>`;
setView(html);
$$('#view [data-go]').forEach(n => n.addEventListener('click', () => go(n.getAttribute('data-go'))));
$$('#view [data-ext]').forEach(n => n.addEventListener('click', () => window.open(n.getAttribute('data-ext'), '_blank')));
$$('#view [data-tcat]').forEach(n => n.addEventListener('click', () => toggleTreeCat(n.getAttribute('data-tcat'))));
}
// Collapse/expand a category in the Contents tree, without a full re-render (preserves scroll).
function toggleTreeCat(cat) {
if (state.treeCollapsed.has(cat)) state.treeCollapsed.delete(cat); else state.treeCollapsed.add(cat);
localStorage.setItem('viz-tree-collapsed', JSON.stringify([...state.treeCollapsed]));
const key = (window.CSS && CSS.escape) ? CSS.escape(cat) : cat;
const wrap = document.querySelector(`#view [data-tcat="${key}"]`)?.closest('.tree-cat');
if (wrap) {
wrap.querySelector(':scope > .tree-cat-head')?.classList.toggle('collapsed');
wrap.querySelector(':scope > .tree-cat-body')?.classList.toggle('collapsed');
}
}
/* ── BOOK VIEW (#/book) β€” the whole wiki as one printable document ─────────
Browser "Save as PDF" (or the pre-rendered download) turns this into a book:
title page β†’ contents β†’ parts (categories) β†’ articles β†’ sources appendix.
The single export path is the browser's "Save as PDF" (always current). */
async function renderBook() {
layoutAside(false);
setView('<div class="loading"><div class="spinner"></div><span id="bkProg">assembling the book…</span></div>');
const parts = state.tax.filter(c => c.nodes.some(n => n.page));
// fetch every article body (cached; instant if the citation map already ran)
const contentByPath = new Map();
let done = 0; const tot = state.pages.length;
await Promise.all(state.pages.map(p => getPageContent(p.path)
.then(c => { if (c) contentByPath.set(p.path, c); }).catch(() => {})
.finally(() => { done++; const e = document.getElementById('bkProg'); if (e) e.textContent = `assembling the book… ${done}/${tot} articles`; })));
const bibIds = new Set();
let totalWords = 0, totalArticles = 0;
const stripH1 = (b) => b.replace(/^\s*#\s+.*(\r?\n)+/, '');
const gatherBib = (b) => { const re = /\[source:([^\]\s]+)\]/g; let m; while ((m = re.exec(b))) bibIds.add(m[1]); };
// Pre-scan for totals
parts.forEach(c => c.nodes.forEach(n => { if (n.page && contentByPath.has(n.page.path)) { const b = parseFrontmatter(contentByPath.get(n.page.path)).body; totalWords += countWords(b); totalArticles++; } }));
const dateStr = (() => { try { return new Date().toISOString().slice(0, 10); } catch (e) { return ''; } })();
let toc = '', bodyHtml = '';
let partNo = 0;
parts.forEach(c => {
partNo++;
const arts = c.nodes.filter(n => n.page && contentByPath.has(n.page.path));
if (!arts.length) return;
const partId = `bk-part-${slugify(c.cat)}`;
toc += `<div class="bk-toc-part"><a data-bk="${partId}"><span class="bk-toc-num">${partNo}</span> ${esc(catDisplay(c.cat))}</a></div>`;
bodyHtml += `<section class="book-part" id="${partId}">
<div class="bk-part-kicker">Part ${partNo}</div>
<h1 class="bk-part-title">${esc(catDisplay(c.cat))}</h1>
${c.description ? `<p class="bk-part-desc">${esc(c.description)}</p>` : ''}
</section>`;
arts.forEach(n => {
const { fm, body } = parseFrontmatter(contentByPath.get(n.page.path));
gatherBib(body);
const title = fm.title || n.page.title || n.slug;
const artId = `bk-${slugify(n.page.path)}`;
toc += `<div class="bk-toc-art"><a data-bk="${artId}">${esc(title)}</a></div>`;
bodyHtml += `<article class="book-article" id="${artId}" data-path="${esc(n.page.path)}">
<div class="bk-art-cat">${esc(catDisplay(c.cat))}</div>
<h1 class="bk-art-title">${esc(title)}</h1>
<div class="prose book-prose">${renderBody(stripH1(body))}</div>
</article>`;
});
});
// Sources appendix (bibliography)
const bib = [...bibIds].sort().map(id => {
const s = state.srcById.get(id); const url = (s && s.original_url) || externalUrl(id);
return `<div class="bk-ref"><span class="bk-ref-id">${esc(id)}</span><span class="bk-ref-title">${esc(s ? (s.title || '') : '')}</span>${url ? `<span class="bk-ref-url">${esc(prettyUrl(url))}</span>` : ''}</div>`;
}).join('');
const html = `
<div class="book-toolbar" role="toolbar">
<a class="btn" href="#/">← back to the wiki</a>
<span class="spacer"></span>
<span class="bk-hint">Choose β€œSave as PDF” in the print dialog for the full book.</span>
<button class="btn btn-sources" id="bkPrint">Save as PDF / Print</button>
</div>
<div class="book" id="book">
<div class="book-titlepage">
<div class="bk-tp-kicker">A living reference Β· reinforcement learning for language models</div>
<h1 class="bk-tp-title">The RL<span class="dot">Β·</span>for<span class="dot">Β·</span>LLMs Wiki</h1>
<div class="bk-tp-sub">From RLHF and preference optimization to reward modeling, verifiable rewards, reasoning, training systems, and the failure modes β€” synthesized from the primary literature, every claim cited.</div>
<div class="bk-tp-meta">
<div><b>${totalArticles}</b> articles Β· <b>${fmtNum(totalWords)}</b> words Β· <b>${bibIds.size}</b> sources</div>
<div>Built and reviewed in the open by a collaboration of AI agents${dateStr ? ` Β· compiled ${dateStr}` : ''}</div>
<div class="bk-tp-src">huggingface.co/datasets/${DATASET}</div>
</div>
</div>
<section class="book-toc" id="bk-contents">
<h1 class="bk-h">Contents</h1>
${toc}
</section>
${bodyHtml}
${bib ? `<section class="book-appendix" id="bk-sources"><h1 class="bk-h">Sources</h1>
<p class="bk-appendix-lead">The ${bibIds.size} sources cited across this book. Each resolves in the wiki to a faithful summary and to the original.</p>
<div class="bk-refs">${bib}</div></section>` : ''}
<div class="book-colophon">The RLΒ·forΒ·LLMs Wiki β€” a public, continuously reviewed dataset. This book is a snapshot; the living version is at huggingface.co/spaces/rl-llm-wiki/rl-wiki.</div>
</div>`;
setView(html);
$$('.book-article').forEach(a => wrapFigures(a, a.dataset.path)); // resolve/caption figures per article
$('#bkPrint')?.addEventListener('click', () => window.print());
$$('#view [data-bk]').forEach(a => a.addEventListener('click', () => document.getElementById(a.getAttribute('data-bk'))?.scrollIntoView({ behavior: 'smooth' })));
}
const slugify = (s) => String(s || '').toLowerCase().replace(/[^\w]+/g, '-').replace(/^-|-$/g, '');
/* ── TOPIC ARTICLE ─────────────────────────────────────── */
async function renderTopic(path) {
loadingView('loading article…');
const content = await getPageContent(path);
const { fm, body } = parseFrontmatter(content);
const meta = state.pages.find(p => p.path === path) || {};
const title = fm.title || meta.title || path.split('/').pop().replace(/\.md$/, '');
const cat = meta.parent || path.split('/').slice(-2, -1)[0] || '';
layoutAside(false); // no right rail β€” single reading column
setView(`
<div class="doc">
<div class="crumbs"><a href="#/">Wiki</a><span class="sep">/</span>
<span>${esc(catDisplay(cat))}</span></div>
<div class="doc-head">
<div class="doc-kicker">Topic Article<button class="share-btn" data-share title="Copy a link to this page">πŸ”— copy link</button></div>
<h1 class="doc-title">${esc(title)}</h1>
<div class="doc-meta-row">${maturityBadge(fm.maturity)}</div>
</div>
<div id="docTop"></div>
<div class="prose" id="prose">${renderBody(body)}</div>
<div id="docBottom"></div>
${footerHTML()}
</div>
`);
wireShare();
const toc = enhanceProse($('#prose'), path);
bindInternalLinks($('#prose'));
// cited sources, in the order they first appear in the article
const citedIds = []; const seenCite = new Set();
$$('#prose .cite[data-src]').forEach(a => { const id = a.dataset.src; if (!seenCite.has(id)) { seenCite.add(id); citedIds.push(id); } });
const base = location.hash.split('~')[0];
// TOP: contents + open questions (both collapsed, unobtrusive)
let top = '';
if (toc.length) top += contentsDisclosure(toc, base);
if (fm.open_questions?.length) top += openQuestionsDisclosure(fm.open_questions, false);
$('#docTop').innerHTML = top;
// BOTTOM: open questions (expanded) + the references + this-page links
let bot = '';
if (fm.open_questions?.length) bot += openQuestionsDisclosure(fm.open_questions, true);
bot += referencesSection(citedIds.length ? citedIds : (fm.sources || []));
bot += `<div class="page-links"><a href="${esc(RESOLVE)}/${esc(path)}" target="_blank" rel="noopener">raw markdown</a><a href="${esc(DATASET_BLOB)}/${esc(path)}" target="_blank" rel="noopener">source on HF β†—</a></div>`;
$('#docBottom').innerHTML = bot;
}
// collapsible <details> blocks used in the article flow
function openQuestionsDisclosure(list, open) {
return `<details class="disclosure oq-d"${open ? ' open' : ''}>
<summary><span class="disc-ic">?</span> Open questions <span class="disc-n">${list.length}</span></summary>
<ul class="oq-list">${list.map(q => `<li>${renderInline(q)}</li>`).join('')}</ul>
</details>`;
}
function contentsDisclosure(toc, base) {
return `<details class="disclosure toc-d">
<summary><span class="disc-ic">≑</span> Contents <span class="disc-n">${toc.length}</span></summary>
<nav class="toc">${toc.map(t => `<a href="${base}~${t.slug}" class="${t.level === 3 ? 'h3' : ''}">${esc(t.text)}</a>`).join('')}</nav>
</details>`;
}
// references (sources cited), listed at the end of the article
function referencesSection(ids) {
if (!ids.length) return '';
return `<section class="refs-section"><h2 class="refs-h">Sources cited<span class="refs-n">${ids.length}</span></h2>
<ol class="refs-list">${ids.map(id => {
const s = state.srcById.get(id); const ext = externalUrl(id);
return `<li><a class="ref-id" href="#/source/${encodeURIComponent(id)}" data-src="${esc(id)}">${esc(id)}</a>` +
(s && s.title ? `<span class="ref-title">${esc(s.title)}</span>` : '') +
(ext ? ` <a class="ref-ext" href="${esc(ext)}" target="_blank" rel="noopener" title="open the original">β†—</a>` : '') + `</li>`;
}).join('')}</ol></section>`;
}
/* ── SOURCE RECORD ─────────────────────────────────────── */
async function renderSource(id) {
loadingView('loading source…');
const meta = state.srcById.get(id);
const summaryPath = meta?.summary_path || `sources/${sanitizeId(id)}.md`;
let content;
try { content = await getText(`${RESOLVE}/${summaryPath}`); }
catch (e) {
layoutAside(false);
const ext = externalUrl(id);
setView(`<div class="error-box">Source <b>${esc(id)}</b> is not in the public dataset yet.
${ext ? `<br><br><a class="btn" href="${esc(ext)}" target="_blank" rel="noopener">open the original β†—</a>` : ''}
<br><br><a class="btn" href="#/">← home</a></div>`);
return;
}
const { fm, body, rawFm } = parseFrontmatter(content);
const notes = refNotes(rawFm);
const title = fm.title || meta?.title || id;
const authors = Array.isArray(fm.authors) ? fm.authors.join(', ') : (fm.authors || '');
layoutAside(true);
setView(`
<div class="crumbs"><a href="#/">Wiki</a><span class="sep">/</span>
<a href="#/sources">sources</a><span class="sep">/</span>
<span>${esc(id)}</span></div>
<div class="doc-head">
<div class="doc-kicker">Source Record<button class="share-btn" data-share title="Copy a link to this source">πŸ”— copy link</button></div>
<h1 class="doc-title">${esc(title)}</h1>
<div class="doc-meta-row">
${fm.type ? `<span class="badge type">${esc(fm.type)}</span>` : ''}
${fm.maturity ? maturityBadge(fm.maturity) : ''}
${fm.year ? `<span class="doc-sub">${esc(fm.year)}</span>` : ''}
${fm.venue ? `<span class="doc-sub">Β· ${esc(fm.venue)}</span>` : ''}
</div>
${authors ? `<div class="doc-authors">${esc(authors)}</div>` : ''}
</div>
<div class="prose" id="prose">${renderBody(body)}</div>
${footerHTML()}
`);
wireShare();
const toc = enhanceProse($('#prose'), summaryPath);
bindInternalLinks($('#prose'));
// Aside: bibliographic metadata
let aside = `<div class="panel"><div class="panel-title">Source</div>`;
aside += metaRow('id', `<code>${esc(id)}</code>`);
if (fm.year) aside += metaRow('year', esc(fm.year));
if (fm.venue) aside += metaRow('venue', esc(fm.venue));
if (fm.reliability) aside += metaRow('reliability', esc(fm.reliability));
if (fm.processed_by) aside += metaRow('processed by', esc(fm.processed_by));
const original = fm.url || meta?.original_url;
if (original) aside += metaRow('original', `<a href="${esc(original)}" target="_blank" rel="noopener">${esc(prettyUrl(original))} β†—</a>`);
aside += `</div>`;
// Cited by β€” which topic articles rely on this source (the graph, inbound).
// The map is built in the background (kicked off in boot); panel fills in when ready.
aside += citedByPanel(id);
// Resources
if (fm.resources && typeof fm.resources === 'object') {
const entries = Object.entries(fm.resources).filter(([, v]) => v);
if (entries.length) {
aside += `<div class="panel"><div class="panel-title">Resources</div><div class="chips">` +
entries.map(([k, v]) => `<a class="chip res ext" href="${esc(v)}" target="_blank" rel="noopener">${esc(k)}</a>`).join('') +
`</div></div>`;
}
}
// License
if (fm.license) aside += `<div class="panel"><div class="panel-title">License</div><div class="meta-row"><div class="v" style="font-size:11.5px;">${esc(fm.license)}</div></div></div>`;
// References found in the source
if (Array.isArray(fm.references_relevant) && fm.references_relevant.length) {
aside += `<div class="panel"><div class="panel-title">References (in-scope)</div><div class="chips">` +
fm.references_relevant.map(rid => sourceChip(rid, notes[rid])).join('') + `</div></div>`;
}
aside += tocPanel(toc);
// Provenance links
aside += `<div class="panel"><div class="panel-title">Provenance</div><div class="chips">
<a class="chip ext" href="${esc(RESOLVE)}/${esc(summaryPath)}" target="_blank" rel="noopener">raw .md</a>
<a class="chip ext" href="${esc(DATASET_BLOB)}/${esc(summaryPath)}" target="_blank" rel="noopener">on HF</a>
${meta?.bucket_path ? `<a class="chip ext" href="${esc(BUCKET)}/${esc(meta.bucket_path)}" target="_blank" rel="noopener">bucket folder</a>` : ''}
</div></div>`;
setAside(aside);
initScrollSpy(toc);
}
/* ── shared bits ───────────────────────────────────────── */
function renderInline(text) {
try { return window.marked ? marked.parseInline(String(text)) : esc(text); }
catch (e) { return esc(text); }
}
function maturityBadge(m) {
const v = (m || '').toLowerCase();
if (!v) return ''; // index sometimes lacks maturity β€” don't show a misleading "unrated"
const cls = v === 'comprehensive' ? 'mat-comprehensive' : v === 'developing' ? 'mat-developing' : 'mat-stub';
return `<span class="badge ${cls}"><span class="b-dot"></span>${esc(v)}</span>`;
}
const plural = (n, w) => `${n} ${w}${n === 1 ? '' : 's'}`;
function sourceChip(id, note) {
const src = state.srcById.get(id);
const t = note ? esc(note) : (src ? esc(sourceTooltip(src, id)) : (state.sourcesLoaded ? 'not yet processed' : esc(id)));
// Resolved, or sources still loading β†’ internal link (optimistic). data-src enables the hovercard.
if (src || !state.sourcesLoaded) return `<a class="chip" href="#/source/${encodeURIComponent(id)}" title="${t}" data-src="${esc(id)}">${esc(id)}</a>`;
const ext = externalUrl(id);
if (ext) return `<a class="chip ext" href="${esc(ext)}" target="_blank" rel="noopener" title="${t} (not processed yet)">${esc(id)}</a>`;
return `<span class="chip" title="${t}">${esc(id)}</span>`;
}
function metaRow(k, vHtml) { return `<div class="meta-row"><div class="k">${esc(k)}</div><div class="v">${vHtml}</div></div>`; }
function prettyUrl(u) { return u.replace(/^https?:\/\//, '').replace(/\/$/, ''); }
// Inbound graph edges: the topic articles that cite this source.
function citedByPanel(id) {
if (!state.citemapReady) return `<div class="panel"><div class="panel-title">Cited by</div><div class="cite-loading">indexing citations…</div></div>`;
const paths = [...(state.citedBy.get(id) || [])];
if (!paths.length) return `<div class="panel"><div class="panel-title">Cited by</div><div class="cite-loading">Not cited by any article yet.</div></div>`;
const arts = paths.map(p => state.pages.find(pg => pg.path === p)).filter(Boolean)
.sort((a, b) => (a._cat || '').localeCompare(b._cat || '') || (a.title || '').localeCompare(b.title || ''));
return `<div class="panel"><div class="panel-title">Cited by Β· ${arts.length} article${arts.length === 1 ? '' : 's'}</div>` +
arts.map(p => `<a class="citedby-row" href="#/topic/${encodeURIComponent(p.path)}">
<span class="cb-title">${esc(p.title || p._node)}</span><span class="cb-cat">${esc(catLabel(p._cat || ''))}</span></a>`).join('') +
`</div>`;
}
function tocPanel(toc) {
if (!toc.length) return '';
return `<div class="panel"><div class="panel-title">On this page</div><div class="toc" id="toc">` +
toc.map(t => `<a href="#${t.slug}" class="${t.level === 3 ? 'h3' : ''}" data-slug="${t.slug}">${esc(t.text)}</a>`).join('') +
`</div></div>`;
}
function footerHTML() {
return `<div class="foot">
<a href="https://huggingface.co/datasets/${DATASET}" target="_blank" rel="noopener">dataset</a>
<a href="https://rl-llm-wiki-rl-dashboard.hf.space" target="_blank" rel="noopener">dashboard</a>
<a href="${API}/v1" target="_blank" rel="noopener">api</a>
<span style="margin-left:auto;color:var(--muted-4)">rendered by the-viz</span>
</div>`;
}
function bindInternalLinks(root) {
// citation links already use #/source/... hash routing β€” nothing extra needed,
// but ensure cite chips don't get target=_blank from the http rule.
}
// Wire the page-level "copy link" button(s) in the current view.
function wireShare() {
$$('#view [data-share]').forEach(b => b.addEventListener('click', () => copyLink(pageShareUrl())));
}
/* ── scrollspy ─────────────────────────────────────────── */
let _spy;
function initScrollSpy(toc) {
if (_spy) { _spy.disconnect(); _spy = null; }
if (!toc.length) return;
const links = new Map($$('#toc a').map(a => [a.getAttribute('data-slug'), a]));
_spy = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (e.isIntersecting) {
links.forEach(a => a.classList.remove('active'));
links.get(e.target.id)?.classList.add('active');
}
});
}, { rootMargin: '-70px 0px -70% 0px', threshold: 0 });
toc.forEach(t => { const h = document.getElementById(t.slug); if (h) _spy.observe(h); });
}
/* ============================================================
SIDEBAR NAV β€” topics only (the first-class citizen). Sources live in
their own browser view (#/sources), reached from the top bar.
============================================================ */
function renderNav() {
const q = ($('#navSearch').value || '').toLowerCase().trim();
const list = $('#navList');
list.innerHTML = '';
renderTopicsNav(list, q);
highlightActive();
}
// On navigation, make the sidebar an accordion: drop manual fold-flips on sub-branches
// (keys with a "/", i.e. L2+) so the active topic's branch auto-unfolds and the others
// fold back in, then re-render and keep the active row in view. Skips while searching.
function syncNavAccordion() {
if (!state.topicTree) { highlightActive(); return; }
const searching = !!($('#navSearch')?.value || '').trim();
if (!searching) {
let changed = false;
[...state.collapsed].forEach(k => { if (k.includes('/')) { state.collapsed.delete(k); changed = true; } });
if (changed) localStorage.setItem('viz-collapsed', JSON.stringify([...state.collapsed]));
}
renderNav();
document.querySelector('#navList .nav-item.active, #navList .cat-link.active')?.scrollIntoView({ block: 'nearest' });
}
/* Topics: the full hierarchy, any depth. Level-1 categories open by default;
sub-categories (level 2+) collapsed by default so deeper nesting is present but
tucked away. Only branches that contain a written article are shown. */
function renderTopicsNav(list, q) {
const root = state.topicTree;
if (!root || !root.children.size) { list.append(el('div', 'nav-empty', state.pages.length ? '' : 'No topics yet.')); return; }
let shownAny = false;
treeChildren(root).forEach(cat => { const e = navBranch(cat, 1, q); if (e) { list.append(e); shownAny = true; } });
if (!shownAny) list.append(el('div', 'nav-empty', q ? 'No matching topics.' : ''));
}
// A leaf article row in the sidebar. Under an active search, the query is bolded in
// the title and every body hit is listed as a clickable snippet that jumps to its
// own section; clicking the row itself jumps to the first hit (or the top).
function navLeafItem(page, fallbackLabel, q) {
const it = el('div', 'nav-item'); it.dataset.path = page.path;
const base = `#/topic/${encodeURIComponent(page.path)}`;
// All body occurrences of the query in this article (empty for a title-only match).
const { infos, total } = q && (state.searchText.get(page.path) || '').includes(q)
? bodyMatchInfos(page.path, q) : { infos: [], total: 0 };
const titleTarget = base + (infos[0] && infos[0].section ? `~${infos[0].section}` : '');
const title = el('span', 'nav-item-title', highlightMatch(page.title || fallbackLabel, q));
if (total > 1) title.append(el('span', 'nav-hit-count', String(total)));
it.append(title);
it.addEventListener('click', () => go(titleTarget)); // clicking the row β†’ first hit / top
if (infos.length) {
const snips = el('div', 'nav-snippets');
infos.forEach(h => {
const s = el('div', 'nav-snippet', h.snippet);
s.addEventListener('click', (e) => { e.stopPropagation(); go(base + (h.section ? `~${h.section}` : '')); });
snips.append(s);
});
if (total > infos.length) snips.append(el('div', 'nav-snippet more', `+${total - infos.length} more match${total - infos.length === 1 ? '' : 'es'}`));
it.append(snips);
}
return it;
}
// Render one internal node (category/sub-category) for the sidebar, recursively.
// A child with its own sub-nodes is a branch (even if it's also an article, e.g.
// topics/x.md alongside topics/x/sub.md) β€” such a node's header links to its article
// AND expands to reveal the sub-articles. Returns the element, or null if empty.
function navBranch(node, depth, q) {
const body = el('div', 'nav-cat-body');
let shown = false;
treeChildren(node).forEach(k => {
if (k.children.size) { // branch: category OR article-with-children
const e = navBranch(k, depth + 1, q);
if (e) { body.append(e); shown = true; }
} else if (k.page) { // plain article leaf
if (q && !matchPage(k.page, q)) return;
body.append(navLeafItem(k.page, catLabel(k.seg), q)); shown = true;
}
// planned leaves (no page, no children) aren't listed in the sidebar
});
const selfMatch = node.page && (!q || matchPage(node.page, q));
if (!shown && !selfMatch) return null;
// Accordion: a branch is open by default if it's a top-level category OR it holds the
// active topic; the caret flips that default. Navigating clears the flips (syncNavAccordion),
// so opening a topic auto-unfolds its branch and folds the others back in.
const dflt = subtreeHasActive(node) || depth < 2;
const open = !!q || (state.collapsed.has(node.key) ? !dflt : dflt);
const wrap = el('div', 'nav-cat');
const head = el('div', `nav-cat-head lvl-${depth}${open ? '' : ' collapsed'}${node.page ? ' has-page' : ''}`);
const label = el('span', 'cat-label'); // fills the row so the fold caret sits at the right
if (node.page) { // an article that also has sub-pages
const a = el('a', 'cat-link', esc(node.page.title || catDisplay(node.seg)));
a.href = `#/topic/${encodeURIComponent(node.page.path)}`; a.dataset.path = node.page.path;
label.append(a);
head.addEventListener('click', () => go(`#/topic/${encodeURIComponent(node.page.path)}`)); // click the row β†’ open the article
} else {
label.textContent = catDisplay(node.seg);
head.addEventListener('click', () => toggleCat(node.key)); // pure category: click the row β†’ toggle
}
head.append(label);
const caret = el('span', 'caret', 'β–Ύ'); // the ONLY fold control, on the right
caret.title = open ? 'Collapse' : 'Expand';
caret.addEventListener('click', (e) => { e.stopPropagation(); e.preventDefault(); toggleCat(node.key); });
head.append(caret);
wrap.append(head);
if (!open) body.classList.add('collapsed');
wrap.append(body);
return wrap;
}
/* ── SOURCES BROWSER (#/sources, main area) ─────────────
The full source list with search, namespace facets, and sort. The palette
(⌘K) reaches any single source; this view is for browsing/scanning. */
let _srcQuery = '';
const SRC_BROWSE_CAP = 200;
function renderSourcesBrowser() {
layoutAside(false);
const n = state.sources.length;
setView(`
<div class="crumbs"><a href="#/">Wiki</a><span class="sep">/</span><span>sources</span></div>
<div class="doc-head">
<div class="doc-kicker">Browse</div>
<h1 class="doc-title">Sources</h1>
<div class="doc-meta-row"><span class="doc-sub">${n} source record${n === 1 ? '' : 's'}${state.sourceTitlesLoaded ? '' : ' Β· <span class="mini-spin"></span> loading titles…'} Β· click any to read its faithful summary</span></div>
</div>
<div class="src-browser">
<div class="src-tools">
<div class="src-search-wrap"><input class="src-search" id="srcSearch" type="text" placeholder="Search sources by title or id…" autocomplete="off" value="${esc(_srcQuery)}"></div>
<div class="sort-switch" id="srcSort"></div>
</div>
<div id="srcResults"></div>
</div>
${footerHTML()}
`);
const sw = $('#srcSort');
[['cites', 'most cited'], ['year', 'newest'], ['az', 'A–Z']].forEach(([k, lbl]) => {
const b = el('button', `sort-opt${state.srcSort === k ? ' active' : ''}`, lbl);
b.addEventListener('click', () => { state.srcSort = k; renderSrcResults(); });
sw.append(b);
});
let st;
const inp = $('#srcSearch');
inp.addEventListener('input', () => { clearTimeout(st); st = setTimeout(() => { _srcQuery = inp.value; renderSrcResults(); }, 110); });
renderSrcResults();
}
function renderSrcResults() {
const box = $('#srcResults'); if (!box) return;
box.innerHTML = '';
if (!state.sourcesLoaded && !state.sources.length) { box.append(el('div', 'nav-empty', '<span class="mini-spin"></span> loading sources…')); return; }
// namespace facets
const nsCounts = {};
state.sources.forEach(s => { const ns = s.id.split(':')[0]; nsCounts[ns] = (nsCounts[ns] || 0) + 1; });
const namespaces = Object.keys(nsCounts).sort();
if (namespaces.length > 1) {
const chips = el('div', 'ns-chips');
const mk = (ns, label, count) => {
const c = el('button', `ns-chip${(state.srcNs === ns) ? ' active' : ''}`, `${esc(label)} <span class="c">${count}</span>`);
c.addEventListener('click', () => { state.srcNs = ns; renderSrcResults(); });
return c;
};
chips.append(mk(null, 'all', state.sources.length));
namespaces.forEach(ns => chips.append(mk(ns, ns, nsCounts[ns])));
box.append(chips);
}
// coverage facet: how many sources are actually woven into topic articles vs orphaned.
// (Needs the citation map; shows once it's built.)
const base = state.sources.filter(s => !state.srcNs || s.id.startsWith(state.srcNs + ':'));
if (state.citemapReady) {
const citedN = base.filter(s => (state.citeCount.get(s.id) || 0) > 0).length;
const orphanN = base.length - citedN;
const chips = el('div', 'ns-chips cov-chips');
const mk = (f, label, count) => {
const c = el('button', `ns-chip${state.srcFilter === f ? ' active' : ''}`, `${esc(label)} <span class="c">${count}</span>`);
c.addEventListener('click', () => { state.srcFilter = f; renderSrcResults(); });
return c;
};
chips.append(mk(null, 'all', base.length));
chips.append(mk('cited', 'in articles', citedN));
chips.append(mk('uncited', 'not yet cited', orphanN));
box.append(chips);
box.append(el('div', 'cov-note', `<b>${citedN}</b> of ${base.length} woven into topic articles Β· <b>${orphanN}</b> captured but not yet cited anywhere`));
} else { state.srcFilter = null; }
const q = (_srcQuery || '').toLowerCase().trim();
let items = base.filter(s => {
const n = state.citeCount.get(s.id) || 0;
if (state.srcFilter === 'cited' && n === 0) return false;
if (state.srcFilter === 'uncited' && n > 0) return false;
return !q || matchSource(s, q);
});
items = sortSources(items, state.srcSort);
const total = items.length;
if (!total) { box.append(el('div', 'nav-empty', q ? 'No matching sources.' : 'No sources yet.')); return; }
const shown = items.slice(0, SRC_BROWSE_CAP);
ensureTitles(shown); // fill titles from frontmatter if /v1/sources hasn't delivered them
box.append(el('div', 'list-meta',
`<span>${total === shown.length ? `${total} source${total === 1 ? '' : 's'}` : `showing ${shown.length} of ${total}`}</span>` +
(total > shown.length ? `<span class="hint">refine your search to narrow</span>` : '')));
const panel = el('div', 'panel'); panel.style.padding = '4px 8px';
shown.forEach(s => {
const row = el('div', 'src-row'); row.dataset.src = s.id;
const n = state.citeCount.get(s.id) || 0;
const yr = sourceYear(s.id);
const meta = [yr || '', n ? `cited ${n}Γ—` : ''].filter(Boolean).join(' Β· ');
// title leads; show the id as a trailing tag only when we actually have a title
// (otherwise the id would appear twice while titles are still loading)
row.innerHTML = `<span class="sr-title">${esc(s.title || s.id)}</span><span class="sr-year">${esc(meta)}</span>${s.title ? `<span class="sr-id">${esc(s.id)}</span>` : ''}`;
row.addEventListener('click', () => go(`#/source/${encodeURIComponent(s.id)}`));
panel.append(row);
});
box.append(panel);
}
function sortSources(items, mode) {
const arr = items.slice();
if (mode === 'year') arr.sort((a, b) => (sourceYear(b.id) || 0) - (sourceYear(a.id) || 0) || b.id.localeCompare(a.id));
else if (mode === 'az') arr.sort((a, b) => (a.title || a.id).localeCompare(b.title || b.id));
else arr.sort((a, b) => (state.citeCount.get(b.id) || 0) - (state.citeCount.get(a.id) || 0) || (a.title || a.id).localeCompare(b.title || b.id));
return arr;
}
// A page matches on its title, its path, or anywhere in its (background-indexed)
// body. `q` is already lowercased by callers.
const matchPage = (p, q) =>
(p.title || '').toLowerCase().includes(q) ||
p.path.toLowerCase().includes(q) ||
(state.searchText.get(p.path) || '').includes(q);
// Every body occurrence of q in a page: a highlighted excerpt + its nearest heading,
// so the sidebar can list all in-document hits and each can jump to its own section.
// Returns { infos: [...], total } β€” total is the true count even when infos is capped.
function bodyMatchInfos(path, q, cap = 20) {
const raw = state.pageContent.get(path), lc = state.searchText.get(path);
if (raw == null || lc == null) return { infos: [], total: 0 };
const text = parseFrontmatter(raw).body; // same body the index was built from β†’ indices align with lc and hits map to visible prose
const infos = []; let total = 0, i = lc.indexOf(q);
while (i >= 0) {
total++;
if (infos.length < cap) {
const start = Math.max(0, i - 30);
let clip = text.slice(start, i + q.length + 40)
.replace(/\[source:[^\]]*\]/g, ' ')
.replace(/[#>*_`~|]+/g, ' ')
.replace(/\s+/g, ' ').trim();
clip = (start > 0 ? '…' : '') + clip + '…';
infos.push({ snippet: highlightMatch(clip, q), section: headingSlugBefore(text, i) });
}
i = lc.indexOf(q, i + q.length);
}
return { infos, total };
}
// Slug of the last ##/###/#### heading before `pos`, replicating enhanceProse's
// slugify + de-dup (and skipping fenced code) so the id matches the rendered anchor.
function headingSlugBefore(md, pos) {
const seen = {};
let found = '', inFence = false, offset = 0;
for (const line of md.split('\n')) {
if (/^\s*(```|~~~)/.test(line)) inFence = !inFence;
else if (!inFence) {
const h = /^(#{2,4})\s+(.+?)\s*#*$/.exec(line);
if (h) {
let slug = h[2].trim().toLowerCase().replace(/[^\w\s-]/g, '').trim().replace(/\s+/g, '-').slice(0, 60) || 'sec';
if (seen[slug] != null) slug = `${slug}-${++seen[slug]}`; else seen[slug] = 0;
if (offset <= pos) found = slug; // last heading at/before the match wins
}
}
offset += line.length + 1; // +1 for the consumed '\n'
}
return found;
}
// Throttled nav re-render, used while background indexing fills in during a search.
let _navRefreshT = null;
function scheduleNavRefresh() {
if (_navRefreshT) return;
_navRefreshT = setTimeout(() => { _navRefreshT = null; if (($('#navSearch').value || '').trim()) renderNav(); }, 200);
}
const matchSource = (s, q) => (s.title || '').toLowerCase().includes(q) || s.id.toLowerCase().includes(q);
function toggleCat(cat) {
if (state.collapsed.has(cat)) state.collapsed.delete(cat); else state.collapsed.add(cat);
localStorage.setItem('viz-collapsed', JSON.stringify([...state.collapsed]));
renderNav();
}
// Toggle the .active highlight on the matching topic nav item, and mark the
// top-bar "sources" button active on the sources view.
function highlightActive() {
const r = parseHash();
$$('.nav-item, .cat-link').forEach(it => {
it.classList.toggle('active', r.view === 'topic' && it.dataset.path === r.path);
});
$('#lnkSources')?.classList.toggle('active', r.view === 'sources');
}
/* ============================================================
SEARCH β€” the sidebar box filters topic titles + bodies; ⌘K / "/" focus it.
============================================================ */
// Bold the matched substring in `text` (used for nav titles + snippets).
// `text` and `raw` are compared case-insensitively.
function highlightMatch(text, raw) {
const q = raw.trim();
if (!q) return esc(text);
const i = text.toLowerCase().indexOf(q.toLowerCase());
if (i < 0) return esc(text);
return esc(text.slice(0, i)) + '<b>' + esc(text.slice(i, i + q.length)) + '</b>' + esc(text.slice(i + q.length));
}
// The search box's ⌘K/βœ• badge, and focus/clear helpers β€” top-level so route()'s
// deep-link handler (applyDeepLinkQuery) can seed the search and sync the badge.
function focusNavSearch() { const s = $('#navSearch'); if (s) { s.focus(); s.select(); } }
function clearNavSearch() {
const s = $('#navSearch'); if (!s) return;
s.value = ''; syncSearchBadge(); renderNav(); refreshDocHighlights(''); s.focus();
}
// The badge is a ⌘K focus hint while empty, and a βœ• clear button once you've typed.
function syncSearchBadge() {
const badge = $('#paletteOpen'); if (!badge) return;
const has = !!($('#navSearch') && $('#navSearch').value);
badge.textContent = has ? 'βœ•' : '⌘K';
badge.classList.toggle('is-clear', has);
badge.title = has ? 'Clear search' : 'Focus search (⌘K)';
}
/* ── mobile nav ────────────────────────────────────────── */
function closeNav() { document.body.classList.remove('nav-open'); }
function toggleNav() { document.body.classList.toggle('nav-open'); }
/* ── theme ─────────────────────────────────────────────── */
function initTheme() {
const saved = localStorage.getItem('viz-theme');
const sysDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
setTheme(saved || (sysDark ? 'dark' : 'light'));
}
function setTheme(t) {
document.documentElement.setAttribute('data-theme', t);
localStorage.setItem('viz-theme', t);
}
function toggleTheme() {
setTheme(document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark');
}
/* ============================================================
BOOT
============================================================ */
function updateCounts() {
const t = state.pages.length, s = state.sources.length, p = state.prs.length;
$('#counts').innerHTML =
`<span class="n">${t}</span> ${t === 1 ? 'topic' : 'topics'}<span class="sep">Β·</span>` +
`<span class="n">${s}</span> ${s === 1 ? 'source' : 'sources'}` +
(p ? `<span class="sep">Β·</span><span class="n">${p}</span> open ${p === 1 ? 'PR' : 'PRs'}` : '');
}
// Critical path β€” fast endpoints only (pages ~0.3s, taxonomy ~0.2s CDN).
// Everything else loads in the background so the UI is interactive immediately.
async function loadCore() {
const pre = window.__prefetch || {};
let [pagesD, taxT] = await Promise.all([
Promise.resolve(pre.pages).catch(() => null),
Promise.resolve(pre.tax).catch(() => null),
]);
// Prefetch failed (transient) β†’ fall back to a direct fetch so the app still loads.
if (!pagesD) { try { pagesD = await getJSON(`${API}/v1/wiki/pages`); } catch (e) { console.warn('pages', e); } }
if (!taxT) { try { taxT = await getText(`${RESOLVE}/taxonomy.yaml`); } catch (e) {} }
if (pagesD && pagesD.items) {
state.pages = pagesD.items.slice();
state.pages.forEach(p => { p.title = prettyTitle(p.title); }); // fix slug-derived acronym casing
state.pages.sort((a, b) => (a.title || a.path).localeCompare(b.title || b.path));
}
try { state.taxonomy = taxT && window.jsyaml ? jsyaml.load(taxT) : null; } catch (e) { state.taxonomy = null; }
buildTaxonomy();
}
const SOURCES_CACHE_KEY = 'viz-sources-v1';
function setSources(items) {
state.sources = items.slice().sort((a, b) => (a.title || a.id).localeCompare(b.title || b.id));
state.srcById = new Map();
state.sources.forEach(s => state.srcById.set(s.id, s));
state.sourcesLoaded = true; // ids known β†’ citations resolve
if (items.some(s => s.title)) state.sourceTitlesLoaded = true; // tree stage has none yet
}
function refreshSourceViews() {
updateCounts();
softRefresh(); // re-renders the current view (incl. the sources browser), upgrading titles/resolution
}
// Reverse the filename sanitization (`:`/`/` β†’ `-`): `arxiv-1707.06347.md` β†’ `arxiv:1707.06347`.
// Exact for arxiv (all current sources); best-effort for doi/url (corrected when /v1/sources lands).
function idFromSourceFile(fn) {
const base = fn.replace(/\.md$/, '');
const i = base.indexOf('-');
return i < 0 ? base : base.slice(0, i) + ':' + base.slice(i + 1);
}
// The slow /v1/sources (~11s) must never gate the Sources tab. Three-stage load,
// each rendering as soon as it has data:
// 1. localStorage cache β†’ instant on repeat visits (has titles)
// 2. dataset tree API β†’ ~0.3s first-visit list (ids; titles fill in at stage 3)
// 3. /v1/sources β†’ authoritative titles; re-caches for next time
async function loadSources() {
let have = false;
try {
const cached = JSON.parse(localStorage.getItem(SOURCES_CACHE_KEY) || 'null');
if (cached && Array.isArray(cached) && cached.length) { setSources(cached); refreshSourceViews(); have = true; }
} catch (e) {}
if (!have) {
try {
const tree = await Promise.resolve((window.__prefetch && window.__prefetch.tree) || getJSON(`https://huggingface.co/api/datasets/${DATASET}/tree/main/sources?recursive=false`)).catch(() => null);
const items = (tree || []).filter(f => f.path && f.path.endsWith('.md'))
.map(f => { const fn = f.path.split('/').pop(); return { id: idFromSourceFile(fn), title: '', summary_path: `sources/${fn}` }; });
if (items.length) { setSources(items); refreshSourceViews(); have = true; }
} catch (e) { /* fall through to the API */ }
}
try {
const d = await getJSON(`${API}/v1/sources`);
if (d && Array.isArray(d.items) && d.items.length) {
setSources(d.items);
try { localStorage.setItem(SOURCES_CACHE_KEY, JSON.stringify(d.items.map(s => ({ id: s.id, title: s.title, summary_path: s.summary_path, original_url: s.original_url, bucket_path: s.bucket_path })))); } catch (e) {}
refreshSourceViews();
}
} catch (e) { console.warn('sources', e); }
}
// prs/queue/leaderboard β€” small, for the home page; independent of sources.
async function loadMeta() {
await Promise.all([
getJSON(`${API}/v1/wiki/prs`).then(d => { state.prs = (d.items || []); }).catch(() => {}),
getJSON(`${API}/v1/queue`).then(d => { state.queueCount = Array.isArray(d.items) ? d.items.length : (d.count ?? null); }).catch(() => {}),
getJSON(`${API}/v1/wiki/leaderboard`).then(d => { const rows = d.rows || (Array.isArray(d) ? d : []); state.contributors = rows.filter(r => (r.changes_merged || 0) > 0 || (r.reviews_given || 0) > 0).length || rows.length; }).catch(() => {}),
getJSON(`${API}/v1/wiki/merges?limit=40`).then(d => { state.updates = (d.items || (Array.isArray(d) ? d : [])).slice(0, 40); }).catch(() => {}),
]);
updateCounts();
if (parseHash().view === 'home') softRefresh();
}
// relative time from a "YYYY-MM-DD HH:MM UTC" (or ISO) timestamp
function relTime(ts) {
if (!ts) return '';
const d = new Date(String(ts).replace(' UTC', 'Z').replace(' ', 'T'));
if (isNaN(d)) return '';
const s = (Date.now() - d.getTime()) / 1000;
if (s < 90) return 'just now';
const m = s / 60; if (m < 60) return `${Math.round(m)}m ago`;
const h = m / 60; if (h < 24) return `${Math.round(h)}h ago`;
const dd = h / 24; if (dd < 30) return `${Math.round(dd)}d ago`;
return d.toISOString().slice(0, 10);
}
// resolve a merged-PR entry to an in-wiki link where possible, else its PR thread
function mergeLink(m) {
const pr = `https://huggingface.co/datasets/${DATASET}/discussions/${m.pr_number}`;
const title = m.title || '';
if (m.kind === 'source') {
let idm = title.match(/(arxiv|doi|hf|url):\s*([^\sβ€”,;]+)/i);
let sid = idm ? `${idm[1].toLowerCase()}:${idm[2]}` : (title.match(/\b(\d{4}\.\d{4,5})\b/) ? `arxiv:${title.match(/\b(\d{4}\.\d{4,5})\b/)[1]}` : null);
if (sid) { sid = sid.replace(/[.,;]$/, ''); if (state.srcById.has(sid)) return { href: `#/source/${encodeURIComponent(sid)}`, ext: false }; }
return { href: pr, ext: true };
}
const cm = title.match(/([a-z0-9-]+\/[a-z0-9-]+)/); // "topic: cat/node …"
if (cm) { const pg = state.pages.find(p => p.path === `topics/${cm[1]}.md`); if (pg) return { href: `#/topic/${encodeURIComponent(pg.path)}`, ext: false }; }
const lt = title.toLowerCase();
const bySlug = state.pages.find(p => p._node && p._node.length > 5 && lt.includes(p._node)); // node slug in the title
return bySlug ? { href: `#/topic/${encodeURIComponent(bySlug.path)}`, ext: false } : { href: pr, ext: true };
}
/* Build the ordered category→node structure from taxonomy.yaml + merged pages.
Taxonomy is non-binding: emergent nodes/categories (written but not in the
taxonomy) are appended so nothing is ever hidden. Degrades gracefully to a
parent-grouping if taxonomy.yaml is missing. */
function buildTaxonomy() {
const tax = state.taxonomy && typeof state.taxonomy === 'object' ? state.taxonomy : {};
const pageByKey = new Map();
state.pages.forEach(p => {
const parts = p.path.replace(/^topics\//, '').replace(/\.md$/, '').split('/');
p._cat = p.parent || parts[0] || 'other';
p._node = parts[parts.length - 1];
pageByKey.set(`${p._cat}/${p._node}`, p);
});
const cats = [];
const seenCat = new Set();
Object.keys(tax).forEach(cat => {
seenCat.add(cat);
const nodesObj = (tax[cat] && tax[cat].nodes) || {};
const nodes = Object.keys(nodesObj).map(slug => ({ slug, scope: nodesObj[slug], page: pageByKey.get(`${cat}/${slug}`) || null }));
state.pages.filter(p => p._cat === cat && !nodesObj[p._node]).forEach(p => nodes.push({ slug: p._node, scope: '', page: p }));
cats.push({ cat, description: (tax[cat] && tax[cat].description) || '', nodes });
});
const em = {};
state.pages.filter(p => !seenCat.has(p._cat)).forEach(p => { (em[p._cat] ||= []).push(p); });
Object.keys(em).sort().forEach(cat => cats.push({ cat, description: '', nodes: em[cat].map(p => ({ slug: p._node, scope: '', page: p })) }));
cats.forEach(c => { c.written = c.nodes.filter(n => n.page).length; c.total = c.nodes.length; });
state.tax = cats;
buildTopicTree();
}
/* Arbitrary-depth topic tree from full paths (topics/a/b/.../node.md) overlaid with
taxonomy categories (descriptions) + planned nodes. Powers the sidebar + Contents
tree so a 3rd (or deeper) hierarchy level renders instead of being flattened away. */
function buildTopicTree() {
const root = { seg: '', key: '', children: new Map() };
const child = (node, seg) => {
if (!node.children.has(seg)) node.children.set(seg, { seg, key: node.key ? `${node.key}/${seg}` : seg, children: new Map() });
return node.children.get(seg);
};
state.pageByKey = new Map(); // "cat/…/node" β†’ page, for cross-ref linkifying
state.pages.forEach(p => {
const key = p.path.replace(/^topics\//, '').replace(/\.md$/, '');
state.pageByKey.set(key, p);
const segs = key.split('/');
let node = root;
segs.forEach(s => { node = child(node, s); });
node.page = p; node.title = p.title; node.article = true; // leaf
});
const tax = state.taxonomy && typeof state.taxonomy === 'object' ? state.taxonomy : {};
root._order = Object.keys(tax);
Object.keys(tax).forEach(cat => {
const cn = child(root, cat);
cn.description = (tax[cat] && tax[cat].description) || '';
const nodesObj = (tax[cat] && tax[cat].nodes) || {};
cn._order = Object.keys(nodesObj);
Object.keys(nodesObj).forEach(slug => {
const ex = cn.children.get(slug);
if (!ex) cn.children.set(slug, { seg: slug, key: `${cat}/${slug}`, children: new Map(), planned: true, scope: nodesObj[slug] });
else if (ex.scope == null) ex.scope = nodesObj[slug];
});
});
state.topicTree = root;
}
// recursive rollup: words + inline refs + article/written/total counts under a node.
// A node can be BOTH an article and a parent (topics/x.md + topics/x/sub.md) β€” count
// its own article AND everything beneath it.
function nodeStats(node) {
const self = (node.article && node.page)
? { words: state.wordCount.get(node.page.path) || 0, refs: state.refCount.get(node.page.path) || 0, articles: 1, written: 1, total: 1 }
: { words: 0, refs: 0, articles: 0, written: 0, total: 0 };
if (!node.children.size) return node.page ? self : { words: 0, refs: 0, articles: 0, written: 0, total: 1 }; // article leaf | planned leaf
const acc = { ...self };
node.children.forEach(c => { const s = nodeStats(c); acc.words += s.words; acc.refs += s.refs; acc.articles += s.articles; acc.written += s.written; acc.total += s.total; });
return acc;
}
// children in taxonomy order where known, else alphabetical
function treeChildren(node) {
const kids = [...node.children.values()];
const ord = node._order || [];
return kids.sort((a, b) => {
const ia = ord.indexOf(a.seg), ib = ord.indexOf(b.seg);
if (ia >= 0 || ib >= 0) return (ia < 0 ? 1e9 : ia) - (ib < 0 ? 1e9 : ib);
return (a.title || catLabel(a.seg)).localeCompare(b.title || catLabel(b.seg));
});
}
// is the active topic somewhere under this node? (to auto-open its branch)
function subtreeHasActive(node) {
const r = parseHash(); if (r.view !== 'topic' || !r.path) return false;
const ap = r.path.replace(/^topics\//, '').replace(/\.md$/, '');
return ap === node.key || ap.startsWith(node.key + '/');
}
// Home "Contents" tree β€” one internal node, recursively, with rollup counts.
// Fully unfolded by default (all levels open); collapsing is opt-in and remembered.
function homeTreeHtml(node, depth, ready) {
const st = nodeStats(node);
const open = !state.treeCollapsed.has(node.key); // front ToC fully unfolded by default (collapse is opt-in)
const rollup = ready ? `<span class="t-count"><b>${fmtNum(st.words)}</b> words Β· <b>${fmtNum(st.refs)}</b> refs</span>` : '';
let inner = node.description ? `<div class="tree-desc">${esc(node.description)}</div>` : '';
let planned = 0;
treeChildren(node).forEach(k => {
if (k.children.size) {
inner += homeTreeHtml(k, depth + 1, ready); // nested sub-category (or article-with-children)
} else if (k.page) {
const pw = state.wordCount.get(k.page.path) || 0, pr = state.refCount.get(k.page.path) || 0;
inner += `<a class="tree-leaf" href="#/topic/${encodeURIComponent(k.page.path)}"><span class="tl-dot"></span><span class="tl-name">${esc(k.page.title || catLabel(k.seg))}</span>${ready ? `<span class="t-count">${fmtNum(pw)} words Β· ${pr} refs</span>` : ''}</a>`;
} else if (planned++ < MAP_PLANNED_CAP) {
inner += `<span class="tree-leaf planned" ${k.scope ? `title="${esc(k.scope)}"` : ''}><span class="tl-dot planned"></span><span class="tl-name">${esc(catDisplay(k.seg))}</span><span class="tl-tag">planned</span></span>`;
}
});
if (planned > MAP_PLANNED_CAP) inner += `<span class="tree-leaf planned"><span class="tl-dot planned"></span><span class="tl-name">+${planned - MAP_PLANNED_CAP} more planned</span></span>`;
// A node that is both an article and a parent gets a linked header (to its own article) + a caret to expand.
const key = esc(node.key);
const dot = node.page ? '<span class="tl-dot"></span>' : ''; // article-parents get the same green dot as leaf articles
const name = node.page
? `<a class="tc-name tc-link" href="#/topic/${encodeURIComponent(node.page.path)}">${esc(node.page.title || catDisplay(node.seg))}</a>`
: `<span class="tc-name">${esc(catDisplay(node.seg))}</span>`;
return `<div class="tree-cat">
<div class="tree-cat-head lvl-${depth}${open ? '' : ' collapsed'}${node.page ? ' has-page' : ''}"${node.page ? '' : ` data-tcat="${key}"`}>
<span class="tc-name-wrap">${dot}${name}</span>
${rollup}
<span class="caret"${node.page ? ` data-tcat="${key}"` : ''} title="${open ? 'Collapse' : 'Expand'}">β–Ύ</span>
</div>
<div class="tree-cat-body${open ? '' : ' collapsed'}">${inner}</div>
</div>`;
}
const catLabel = (c) => prettyTitle((c || '').replace(/-/g, ' '));
// Sentence-cased category for headings, e.g. "reward-modeling" β†’ "Reward modeling".
const catDisplay = (c) => { const s = catLabel(c); return s.charAt(0).toUpperCase() + s.slice(1); };
// The /v1/wiki/pages list gives slug-derived titles ("Dpo And Offline Po"), which
// mangle domain acronyms. Restore proper casing for known terms + lowercase the
// little connector words. (Article headers use the real frontmatter title instead.)
const ACRONYMS = {
rl:'RL', llm:'LLM', llms:'LLMs', rlhf:'RLHF', rlaif:'RLAIF', rlvr:'RLVR', dpo:'DPO', ipo:'IPO',
kto:'KTO', orpo:'ORPO', simpo:'SimPO', ppo:'PPO', grpo:'GRPO', trpo:'TRPO', gae:'GAE', kl:'KL',
sft:'SFT', rm:'RM', orm:'ORM', prm:'PRM', mdp:'MDP', bon:'BoN', raft:'RAFT', rft:'RFT', ai:'AI',
mcts:'MCTS', cot:'CoT', gpt:'GPT', api:'API', po:'PO', nlp:'NLP', qa:'QA', r1:'R1', o1:'o1',
dapo:'DAPO', gspo:'GSPO', vapo:'VAPO', deepseek:'DeepSeek', openai:'OpenAI', mle:'MLE', bt:'BT',
};
const SMALL_WORDS = new Set(['and','or','of','for','the','to','vs','in','on','a','an','with','from','as','at','via']);
function prettyTitle(s) {
return String(s == null ? '' : s).split(' ').map((w, i) => {
const key = w.toLowerCase().replace(/[^a-z0-9]/g, '');
if (ACRONYMS[key]) return w.replace(/[A-Za-z0-9]+/, ACRONYMS[key]);
if (i > 0 && SMALL_WORDS.has(key)) return w.toLowerCase();
return w;
}).join(' ');
}
/* ============================================================
CITATION MAP β€” derive source↔article edges from article bodies.
The API exposes no citation graph, but every article body carries
[source:<id>]; scanning the (bounded) set of articles once gives us
load-bearingness (cite count), cited-by backlinks, and category links.
Built lazily + cached so a reader who opens one article never pays for it.
============================================================ */
async function ensureCitemap() {
if (state.citemapStarted) return;
state.citemapStarted = true;
const paths = state.pages.map(p => p.path);
let i = 0;
const worker = async () => {
while (i < paths.length) {
const path = paths[i++];
try {
const content = await getPageContent(path);
const ids = new Set();
const re = /\[source:([^\]\s]+)\]/g; let m, refs = 0;
while ((m = re.exec(content))) { ids.add(m[1]); refs++; } // distinct β†’ citedBy; occurrences β†’ refCount
ids.forEach(id => { if (!state.citedBy.has(id)) state.citedBy.set(id, new Set()); state.citedBy.get(id).add(path); });
state.refCount.set(path, refs);
state.wordCount.set(path, countWords(content));
state.searchText.set(path, parseFrontmatter(content).body.toLowerCase()); // body only (frontmatter/open-questions excluded) so every hit maps to visible prose
if (($('#navSearch').value || '').trim()) scheduleNavRefresh(); // live-fill an in-progress search
} catch (e) { /* skip unreadable article */ }
}
};
// Low concurrency (3): the browser caps ~6 connections/host, and we must leave
// room for user-initiated navigation fetches β€” background indexing must never
// starve the foreground.
await Promise.all(Array.from({ length: Math.min(3, paths.length) }, worker));
state.citedBy.forEach((set, id) => state.citeCount.set(id, set.size));
state.citemapReady = true;
refreshCitationViews();
}
// Prose word count β€” excludes frontmatter, code, math, citations, and markdown syntax.
function countWords(md) {
let t = String(md || '').replace(/^ο»Ώ?---[\s\S]*?\n---\s*\n/, '');
t = t.replace(/```[\s\S]*?```/g, ' ').replace(/\$\$[\s\S]*?\$\$/g, ' ').replace(/\$[^\n$]*?\$/g, ' ');
t = t.replace(/\[source:[^\]]*\]/g, ' ').replace(/[#>*_`|~]+/g, ' ');
return t.split(/\s+/).filter(w => /[A-Za-z0-9]/.test(w)).length;
}
const fmtNum = (n) => (n || 0).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
// Re-render whatever's on screen that depends on citation data, now that it's ready.
function refreshCitationViews() {
const r = parseHash();
if (r.view === 'source' || r.view === 'home' || r.view === 'sources') softRefresh();
}
// Year decoded for free from arxiv ids (YYMM.NNNNN); else from cited frontmatter is too costly, so null.
function sourceYear(id) {
const m = /^arxiv:(\d{2})(\d{2})\./.exec(id);
if (!m) return null;
const yy = +m[1];
return yy >= 91 ? 1900 + yy : 2000 + yy; // arXiv started 1991; everything else is 20xx
}
// Categories (taxonomy) that cite a source, via the topics that cite it.
function sourceCategories(id) {
const cats = new Set();
(state.citedBy.get(id) || new Set()).forEach(path => {
const p = state.pages.find(pg => pg.path === path);
if (p && p._cat) cats.add(p._cat);
});
return [...cats];
}
async function boot() {
initTheme();
setupMarked();
// events β€” all optional-chained so a missing element can never abort boot and
// blank the whole app (a stale index.html must degrade, not brick).
$('#themeToggle')?.addEventListener('click', toggleTheme);
$('#menuToggle')?.addEventListener('click', toggleNav);
$('#navScrim')?.addEventListener('click', closeNav);
$('#brandHome')?.addEventListener('click', () => go('#/'));
let st;
// One search box: filters topic titles + bodies, and live-highlights the open article.
$('#navSearch')?.addEventListener('input', () => {
ensureCitemap(); // kick off body indexing on first keystroke so content matches fill in fast
syncSearchBadge();
clearTimeout(st); st = setTimeout(() => { renderNav(); refreshDocHighlights($('#navSearch').value); }, 120);
});
window.addEventListener('hashchange', route);
// The ⌘K badge doubles as a βœ• clear button once a query is typed.
$('#paletteOpen')?.addEventListener('click', () => { $('#navSearch')?.value ? clearNavSearch() : focusNavSearch(); });
// ⌘K (and "/") focus the top-left search β€” one box that filters titles + bodies.
window.addEventListener('keydown', (e) => {
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); focusNavSearch(); return; }
if (e.key === 'Escape' && document.activeElement === $('#navSearch') && $('#navSearch')?.value) { clearNavSearch(); return; }
const typing = /^(input|textarea)$/i.test(document.activeElement?.tagName || '');
if (e.key === '/' && !typing) { e.preventDefault(); focusNavSearch(); }
});
initHovercards();
// Sources/meta are independent of pages+taxonomy β€” start them NOW (in parallel with
// loadCore) so the source list + citation resolution are ready ASAP, not gated on
// the critical render path. (softRefresh is a no-op until the first render lands.)
loadSources().catch(e => console.warn('sources', e));
loadMeta().catch(e => console.warn('meta', e));
// Critical path: render as soon as topics + taxonomy are in (sub-second).
try {
await loadCore();
} catch (e) {
showError('Could not reach the wiki API. ' + (e.message || ''));
return;
}
updateCounts();
renderNav();
route();
state.firstRenderDone = true;
// Citation map a bit after first paint so its bulk fetches don't contend with the
// current view's content fetch for the per-host connection budget.
setTimeout(() => ensureCitemap(), 1500);
}
// marked & friends are loaded with `defer`; run after DOM + scripts are ready.
if (document.readyState === 'loading') window.addEventListener('DOMContentLoaded', boot);
else boot();