AdithyaSK's picture
AdithyaSK HF Staff
Unify dataset+task into one master-detail workspace; UI polish
78d005c
/* Harbor Hub β€” SPA frontend. Vanilla JS, hash-routed, talks to the FastAPI API. */
'use strict';
const APP = document.getElementById('app');
/* ── tiny helpers ─────────────────────────────────── */
const esc = (s) => String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
const fmtNum = (n) => (n == null || n < 0) ? 'β€”' : n.toLocaleString();
const enc = encodeURIComponent;
const qs = (o) => Object.entries(o).filter(([, v]) => v != null && v !== '').map(([k, v]) => `${k}=${enc(v)}`).join('&');
async function api(path) {
const r = await fetch(path);
if (!r.ok) {
let msg = `${r.status}`;
try { msg = (await r.json()).detail || msg; } catch {}
throw new Error(msg);
}
return r.json();
}
const ICON = {
copy: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/></svg>',
check: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M20 6L9 17l-5-5"/></svg>',
search: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4-4"/></svg>',
file: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg>',
dir: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 7a2 2 0 0 1 2-2h4l2 3h8a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>',
info: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M12 16v-4M12 8h.01"/></svg>',
back: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg>',
next: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>',
term: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 17l6-6-6-6M12 19h8"/></svg>',
panel: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M9 4v16"/></svg>',
refresh: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-3-6.7L21 8"/><path d="M21 3v5h-5"/></svg>',
};
function copyButton(text, cls = 'copy') {
const b = document.createElement('button');
b.className = cls; b.innerHTML = ICON.copy; b.title = 'Copy';
b.onclick = (e) => {
e.stopPropagation(); e.preventDefault();
navigator.clipboard.writeText(text).then(() => {
b.innerHTML = ICON.check; b.classList.add('copied');
setTimeout(() => { b.innerHTML = ICON.copy; b.classList.remove('copied'); }, 1100);
});
};
return b;
}
/* ── curated example datasets (shown as bubbles) ──── */
const EXAMPLES = [
{ label: 'Terminal-Bench 2.0', uri: 'harborframework/terminal-bench-2.0' },
{ label: 'TaskTrove', uri: 'open-thoughts/TaskTrove' },
{ label: 'Repo2RLEnv Β· PR diffs', uri: 'AdithyaSK/repo2rlenv-v083-pr_diff' },
{ label: 'TitanBench', uri: 'billshockley/titanbench' },
{ label: 'Harbor tasks demo', uri: 'gh://adithya-s-k/harbor-tasks-demo' },
];
function srcTag(uri) {
if (uri.startsWith('gh://') || uri.includes('github.com')) return 'gh';
if (uri.startsWith('harbor://')) return 'harbor';
return 'hf';
}
function exampleChips() {
const wrap = document.createElement('div'); wrap.className = 'chips';
EXAMPLES.forEach(ex => {
const tag = srcTag(ex.uri);
const c = document.createElement('button'); c.className = 'chip'; c.title = ex.uri;
c.innerHTML = `<span class="src ${tag}">${tag}</span><span>${esc(ex.label)}</span>`;
c.onclick = () => { location.hash = `dataset?uri=${enc(ex.uri)}`; };
wrap.appendChild(c);
});
return wrap;
}
/* ── theme ────────────────────────────────────────── */
function applyTheme(mode) {
const sys = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', mode === 'system' ? sys : mode);
document.querySelectorAll('#theme-toggle button').forEach(b =>
b.classList.toggle('active', b.dataset.mode === mode));
}
(function initTheme() {
let mode = localStorage.getItem('hh-theme') || 'dark';
applyTheme(mode);
document.getElementById('theme-toggle').addEventListener('click', (e) => {
const b = e.target.closest('button'); if (!b) return;
mode = b.dataset.mode; localStorage.setItem('hh-theme', mode); applyTheme(mode);
});
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
if ((localStorage.getItem('hh-theme') || 'dark') === 'system') applyTheme('system');
});
})();
/* ── data row with lazy task count ────────────────── */
function datasetRow(id, count) {
const row = document.createElement('div');
row.className = 'row';
row.onclick = () => { location.hash = `dataset?uri=${enc(id)}`; };
const name = document.createElement('span'); name.className = 'name'; name.textContent = id;
row.appendChild(name);
row.appendChild(copyButton(id));
const t = document.createElement('span'); t.className = 'tasks';
if (count == null) { t.innerHTML = '<span class="spin">Β·Β·Β·</span>'; t.dataset.lazy = id; }
else t.textContent = fmtNum(count);
row.appendChild(t);
return row;
}
// Fill in lazy counts for visible rows, throttled.
async function fillCounts(container) {
const pending = [...container.querySelectorAll('.tasks[data-lazy]')];
let i = 0;
const worker = async () => {
while (i < pending.length) {
const cell = pending[i++]; const id = cell.dataset.lazy; delete cell.dataset.lazy;
try { const r = await api(`/api/hub/count?id=${enc(id)}`); cell.textContent = fmtNum(r.tasks); }
catch { cell.textContent = 'β€”'; }
}
};
await Promise.all([worker(), worker(), worker(), worker()]); // 4 in parallel
}
/* ── routes ───────────────────────────────────────── */
function setActiveNav(name) {
document.querySelectorAll('.nav .links a').forEach(a => a.classList.toggle('active', a.dataset.nav === name));
}
async function renderHome() {
setActiveNav('home');
// Resolve the public base URL: on a HF Space this is the real .hf.space host,
// so deep-link / badge examples don't show localhost.
let origin = location.origin;
try { const cfg = await api('/api/config'); if (cfg.space_host) origin = `https://${cfg.space_host}`; } catch {}
const badgeUrl = 'https://img.shields.io/badge/%F0%9F%A4%97%20Harbor%20Visualiser-View%20Tasks-ffd21e';
const deepLink = `${origin}/?dataset=YOUR_DATASET_ID`;
const badgeMd = `[![Open in Harbor Visualiser](${badgeUrl})](${deepLink})`;
APP.innerHTML = `
<div class="hero">
<div class="mark">πŸ€—</div>
<h1><span class="hf">Hugging Face</span> Harbor Visualiser</h1>
<p>Visualise <a href="https://www.harborframework.com" target="_blank" rel="noopener" class="hl">Harbor&nbsp;β†—</a> task-spec datasets <strong style="color:var(--text)">straight from the Hugging&nbsp;Face&nbsp;Hub</strong> β€” metadata, instructions, oracle patches, tests &amp; Dockerfiles. Also works with GitHub repos and local paths. No bulk download, always the latest.</p>
</div>
<div class="search" id="load-box">
${ICON.search}
<input id="load-input" placeholder="Load any dataset β€” owner/name Β· hf:// Β· gh://owner/repo Β· harbor://org/name Β· /local/path" />
<span class="kbd">↡</span>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;margin:30px 0 14px">
<h2 style="margin:0">Examples</h2>
<a class="faint hl" href="#/datasets" style="font-size:13px">Browse all Harbor datasets β†’</a>
</div>
<div id="examples"></div>
<div class="howto">
<h2>Link your dataset to the visualiser</h2>
<div class="steps">
<div class="step">
<h3>Deep-link any dataset</h3>
<p>Append <code>?dataset=&lt;owner&gt;/&lt;name&gt;</code> to open straight into a dataset's tasks β€” handy from a dataset card or docs.</p>
<div class="snippet"><code id="snip-link">${esc(origin)}/?dataset=&lt;owner&gt;/&lt;name&gt;</code><span id="copy-link"></span></div>
</div>
<div class="step">
<h3>Add a badge to your dataset card</h3>
<p>Paste this Markdown into your dataset README so a πŸ€— badge always links here:</p>
<span class="badge-preview"><span class="l">πŸ€— Harbor Visualiser</span><span class="r">View Tasks</span></span>
<div class="snippet"><code id="snip-badge">${esc(badgeMd)}</code><span id="copy-badge"></span></div>
</div>
</div>
</div>
<div class="footer">
A read-only visualiser for <a href="https://www.harborframework.com" target="_blank" rel="noopener" class="hl">Harbor</a>
task-spec datasets β€” the format used by Harbor for agent evaluation &amp; RL environments.
Runs on Hugging Face Spaces Β· not affiliated with the Harbor project.
</div>
`;
document.getElementById('copy-link').appendChild(copyButton(`${origin}/?dataset=<owner>/<name>`));
document.getElementById('copy-badge').appendChild(copyButton(badgeMd));
document.getElementById('examples').appendChild(exampleChips());
const input = document.getElementById('load-input');
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && input.value.trim()) location.hash = `dataset?uri=${enc(input.value.trim())}`;
});
}
let _hubCache = null;
async function renderDatasets(params) {
setActiveNav('datasets');
const sort = params.get('sort') || 'downloads';
APP.innerHTML = `
<div class="page">
<h1>Datasets</h1>
<p class="muted" style="margin:-8px 0 18px;font-size:13.5px">Search across every <strong style="color:var(--text)">Harbor-tagged dataset on the Hugging&nbsp;Face&nbsp;Hub</strong> β€” the live <code style="background:var(--panel-2);padding:1px 6px;border-radius:4px">other=harbor</code> filter.</p>
<div id="ds-examples" style="margin-bottom:20px"></div>
<div class="search">
${ICON.search}
<input id="ds-search" placeholder="Search Harbor datasets on the Hub…" autofocus />
<select id="ds-sort">
<option value="downloads">Most downloads</option>
<option value="likes">Most likes</option>
<option value="lastModified">Recently updated</option>
</select>
<span class="kbd">⌘K</span>
</div>
<div class="card" id="ds-table"><div class="loading"><span class="spinner"></span>loading…</div></div>
<div class="hint" style="margin-top:18px">
<span class="ic">${ICON.info}</span>
<span><strong style="color:var(--text)">Want your dataset to show up here?</strong> Add the <code>harbor</code> tag to your dataset card's metadata (<code>tags: [harbor]</code> in the README front-matter) and it'll appear in this list automatically.</span>
</div>
</div>`;
document.getElementById('ds-examples').appendChild(exampleChips());
const tbl = document.getElementById('ds-table');
const search = document.getElementById('ds-search');
const sortSel = document.getElementById('ds-sort'); sortSel.value = sort;
async function load() {
tbl.innerHTML = '<div class="loading"><span class="spinner"></span>loading…</div>';
try {
const { datasets } = await api(`/api/hub/datasets?${qs({ sort: sortSel.value, limit: 1000 })}`);
_hubCache = datasets; draw(datasets);
} catch (e) { tbl.innerHTML = `<div class="errbox">${esc(e.message)}</div>`; }
}
function draw(list) {
tbl.innerHTML = '<div class="thead"><span>Dataset</span><span class="col-tasks">Tasks</span></div>';
if (!list.length) { tbl.innerHTML += '<div class="empty">no matching datasets</div>'; return; }
list.slice(0, 300).forEach(d => tbl.appendChild(datasetRow(d.id, null)));
if (list.length > 300) tbl.innerHTML += `<div class="empty">showing 300 of ${list.length} β€” refine your search</div>`;
fillCounts(tbl);
}
let t;
search.addEventListener('input', () => {
clearTimeout(t);
t = setTimeout(() => {
const q = search.value.trim().toLowerCase();
draw(q ? _hubCache.filter(d => d.id.toLowerCase().includes(q)) : _hubCache);
}, 120);
});
sortSel.addEventListener('change', load);
await load();
}
/* ── task viewer (file tree + content) ────────────── */
const LANG = { toml: 'ini', diff: 'diff', patch: 'diff', sh: 'bash', bash: 'bash', py: 'python', json: 'json', yaml: 'yaml', yml: 'yaml', md: 'markdown', js: 'javascript', ts: 'typescript', html: 'xml', css: 'css' };
function langFor(path) {
if (path.endsWith('Dockerfile')) return 'dockerfile';
const ext = path.split('.').pop().toLowerCase();
return LANG[ext] || 'plaintext';
}
function harborCmd(kind, ident, taskId) {
if (kind === 'gh') return `harbor run --task-git-url https://github.com/${ident}.git -i ${taskId} -a oracle`;
if (kind === 'local') return `harbor run -p ${ident} -i ${taskId} -a oracle`;
// hf: pull from the Hub, then run the single task with the oracle agent
const dir = ident.split('/').pop();
return `huggingface-cli download ${ident} --repo-type dataset --local-dir ${dir} && harbor run -p ${dir} -i ${taskId} -a oracle`;
}
// Unified dataset+task workspace: tasks side-panel (master) + file tree + content (detail).
// Used by BOTH the `dataset?uri=` route (no task β†’ empty detail) and the
// `task?uri=&task=` route (task preselected). Switching tasks happens in place.
let _taskSiblings = { uri: null, tasks: [], ident: null, kind: null };
async function renderWorkspace(params) {
setActiveNav('datasets');
const uri = params.get('uri');
let task = params.get('task') || null;
let initialFile = params.get('f');
APP.innerHTML = `<div class="page"><div class="loading"><span class="spinner"></span>loading…
<span class="sub">Loading the Harbor spec β€” a few seconds to a minute for large datasets (the more tasks, the longer the listing).</span>
</div></div>`;
// Sibling task list (the panel) + canonical ident/kind (run command).
// Cached per-uri so flipping between tasks doesn't refetch the list.
if (_taskSiblings.uri !== uri) {
try {
const ds = await api(`/api/dataset?uri=${enc(uri)}`);
_taskSiblings = { uri, tasks: ds.tasks || [], ident: ds.ident, kind: ds.kind };
} catch (e) {
APP.querySelector('.page').innerHTML = `<div class="crumb"><a href="#/datasets">Datasets</a></div><div class="errbox">Failed to load <b>${esc(uri)}</b>: ${esc(e.message)}</div>`;
return;
}
}
const siblings = _taskSiblings.tasks;
const ident = _taskSiblings.ident || uri;
const kind = _taskSiblings.kind || 'hf';
const page = APP.querySelector('.page');
// Honour the collapse pref only when a task is open; with no task the panel is the focus.
const collapsed = task && localStorage.getItem('hh-tasks-collapsed') === '1';
page.innerHTML = `
<div class="crumb">
<button class="nav-btn ghost" id="toggle-tasks" title="Toggle task list">${ICON.panel}</button>
<a href="#/datasets">Datasets</a><span class="sep">/</span>
<a href="#dataset?uri=${enc(uri)}" id="crumb-ident">${esc(ident)}</a>
<span class="pill">${siblings.length} tasks</span>
<span id="crumb-task-wrap"></span>
<span class="pos" id="crumb-pos" style="margin-left:auto"></span>
<button class="nav-btn ghost" id="ws-refresh" title="Refresh dataset">${ICON.refresh}</button>
</div>
<div class="runbar" id="runbar" hidden>
<span class="lbl">${ICON.term}</span>
<code id="run-cmd"></code>
<span id="run-copy"></span>
</div>
<div class="taskview${collapsed ? ' collapsed' : ''}" id="taskview">
<div class="tasks-panel" id="tasks-panel">
<div class="tp-head">Tasks <span class="faint">${siblings.length}</span></div>
<div class="tp-search">${ICON.search}<input id="tp-search" placeholder="Filter tasks…" autofocus /></div>
<div class="tp-list" id="tp-list"></div>
</div>
<div class="tree" id="tree"></div>
<div class="content" id="content"></div>
</div>`;
const taskview = document.getElementById('taskview');
const tpList = document.getElementById('tp-list');
const tree = document.getElementById('tree');
const content = document.getElementById('content');
const runbar = document.getElementById('runbar');
const runCode = document.getElementById('run-cmd');
const runCopyHolder = document.getElementById('run-copy');
document.getElementById('toggle-tasks').onclick = () => {
taskview.classList.toggle('collapsed');
localStorage.setItem('hh-tasks-collapsed', taskview.classList.contains('collapsed') ? '1' : '0');
};
document.getElementById('ws-refresh').onclick = async () => {
try {
const ds = await api(`/api/dataset?${qs({ uri, refresh: 1 })}`);
_taskSiblings = { uri, tasks: ds.tasks || [], ident: ds.ident, kind: ds.kind };
} catch (e) { alert('refresh failed: ' + e.message); return; }
renderWorkspace(params);
};
// ── tasks side panel ──
function drawPanel(filter = '') {
tpList.innerHTML = '';
const q = filter.trim().toLowerCase();
const list = q ? siblings.filter(s => s.toLowerCase().includes(q)) : siblings;
list.slice(0, 1000).forEach(tid => {
const r = document.createElement('div');
r.className = 'tp-item' + (tid === task ? ' active' : '');
r.textContent = tid; r.title = tid; r.dataset.tid = tid;
r.onclick = () => { if (tid !== task) loadDetail(tid, null); };
tpList.appendChild(r);
});
if (list.length > 1000) {
const m = document.createElement('div'); m.className = 'empty'; m.textContent = `showing 1000 of ${list.length} β€” filter to narrow`;
tpList.appendChild(m);
}
}
drawPanel();
const tps = document.getElementById('tp-search');
let ft;
tps.addEventListener('input', () => { clearTimeout(ft); ft = setTimeout(() => drawPanel(tps.value), 100); });
function syncPanelActive(tid) {
tpList.querySelectorAll('.tp-item').forEach(n => n.classList.toggle('active', n.dataset.tid === tid));
const a = tpList.querySelector('.tp-item.active'); if (a) a.scrollIntoView({ block: 'nearest' });
}
// Empty (no task selected) β€” master-detail "nothing selected" state.
function showEmpty() {
task = null;
runbar.hidden = true;
syncPanelActive(null);
document.getElementById('crumb-task-wrap').innerHTML = '';
document.getElementById('crumb-pos').textContent = '';
history.replaceState(null, '', '#' + `dataset?${qs({ uri })}`);
tree.innerHTML = '';
content.innerHTML = `<div class="emptysel"><div class="ic">${ICON.panel}</div>
<p>Select a task from the list to view its spec, files & run command.</p></div>`;
}
// ── load one task's detail into the tree + content (no full re-render) ──
async function loadDetail(tid, wantFile) {
task = tid;
syncPanelActive(tid);
document.getElementById('crumb-task-wrap').innerHTML =
`<span class="sep">/</span><span id="crumb-task">${esc(tid)}</span><span id="crumb-diff"></span>`;
const i = siblings.indexOf(tid);
document.getElementById('crumb-pos').textContent = i >= 0 ? `${i + 1} / ${siblings.length}` : '';
history.replaceState(null, '', '#' + `task?${qs({ uri, task: tid })}`);
const cmd = harborCmd(kind, ident, tid);
runbar.hidden = false;
runCode.textContent = cmd;
runCopyHolder.innerHTML = '';
const rc = copyButton(cmd);
rc.addEventListener('click', () => { runbar.classList.add('copied'); setTimeout(() => runbar.classList.remove('copied'), 1100); });
runCopyHolder.appendChild(rc);
tree.innerHTML = '';
content.innerHTML = `<div class="loading"><span class="spinner"></span>loading task…</div>`;
let t;
try { t = await api(`/api/task?${qs({ uri, task: tid })}`); }
catch (e) { content.innerHTML = `<div class="errbox">${esc(e.message)}</div>`; return; }
if (task !== tid) return; // a newer click superseded this fetch
const diffEl = document.getElementById('crumb-diff');
if (diffEl) diffEl.innerHTML = t.difficulty ? ` <span class="pill">${esc(t.difficulty)}</span>` : '';
buildDetail(t, wantFile);
}
function buildDetail(t, wantFile) {
const files = t.files || {};
const paths = Object.keys(files).sort();
tree.innerHTML = `<div class="thead2">${esc(t.id)}</div>`;
function node(label, indent, type, onClick, active) {
const n = document.createElement('div');
n.className = 'tnode' + (type === 'dir' ? ' dir' : '') + (active ? ' active' : '');
n.style.paddingLeft = (14 + indent * 16) + 'px';
n.innerHTML = (type === 'dir' ? ICON.dir : type === 'info' ? ICON.info : ICON.file) + `<span>${esc(label)}</span>`;
if (onClick) n.onclick = onClick;
return n;
}
const nodes = {};
function setHashFile(f) { return `task?${qs({ uri, task: t.id, f })}`; }
function select(id) {
Object.values(nodes).forEach(n => n.classList.remove('active'));
if (nodes[id]) nodes[id].classList.add('active');
if (id === '__overview__') showOverview(); else showFile(id);
}
const ov = node('Overview', 0, 'info', () => { history.replaceState(null, '', '#' + setHashFile('__overview__')); select('__overview__'); });
nodes['__overview__'] = ov; tree.appendChild(ov);
const groups = {}; const top = [];
paths.forEach(p => { if (p.includes('/')) { const f = p.split('/')[0]; (groups[f] = groups[f] || []).push(p); } else top.push(p); });
top.forEach(p => { const n = node(p, 0, 'file', () => { history.replaceState(null, '', '#' + setHashFile(p)); select(p); }); nodes[p] = n; tree.appendChild(n); });
Object.keys(groups).sort().forEach(folder => {
tree.appendChild(node(folder + '/', 0, 'dir'));
groups[folder].sort().forEach(p => { const n = node(p.split('/').slice(1).join('/'), 1, 'file', () => { history.replaceState(null, '', '#' + setHashFile(p)); select(p); }); nodes[p] = n; tree.appendChild(n); });
});
function showOverview() {
const rows = [];
const add = (k, v) => { if (v != null && v !== '' && !(Array.isArray(v) && !v.length)) rows.push([k, v]); };
add('Task id', t.id); add('Name', t.name); add('Org', t.org); add('Version', t.version);
add('Difficulty', t.difficulty); add('Category', t.category);
add('Agent timeout', t.agent_timeout_sec != null ? t.agent_timeout_sec + 's' : null);
add('Verifier timeout', t.verifier_timeout_sec != null ? t.verifier_timeout_sec + 's' : null);
let html = `<div class="fhead"><span class="path">${ICON.info} Overview</span></div>`;
if (t.description) html += `<div class="md">${marked.parse(t.description)}</div>`;
html += '<table class="kv">';
rows.forEach(([k, v]) => html += `<tr><td>${esc(k)}</td><td>${esc(v)}</td></tr>`);
if (t.keywords && t.keywords.length) html += `<tr><td>Keywords</td><td>${t.keywords.map(k => `<span class="kw">${esc(k)}</span>`).join('')}</td></tr>`;
if (t.repo2env) html += `<tr><td>repo2env</td><td><pre style="margin:0;padding:0;background:none">${esc(JSON.stringify(t.repo2env, null, 2))}</pre></td></tr>`;
html += '</table>';
const instr = files['instruction.md'] || t.instruction_inline;
if (instr) html += `<div class="fhead"><span class="path">${ICON.file} instruction.md</span></div><div class="md">${marked.parse(instr)}</div>`;
content.innerHTML = html;
}
function showFile(path) {
const body = files[path] != null ? files[path] : (path === 'task.toml' ? t.task_toml_raw : '');
const fhead = document.createElement('div'); fhead.className = 'fhead';
fhead.innerHTML = `<span class="path">${ICON.file} ${esc(path)}</span>`;
fhead.appendChild(copyButton(body));
content.innerHTML = '';
content.appendChild(fhead);
if (path.endsWith('.md')) {
const d = document.createElement('div'); d.className = 'md'; d.innerHTML = marked.parse(body); content.appendChild(d);
} else {
const pre = document.createElement('pre'); const code = document.createElement('code');
code.className = 'language-' + langFor(path); code.textContent = body;
pre.appendChild(code); content.appendChild(pre);
try { hljs.highlightElement(code); } catch {}
}
content.scrollTop = 0;
}
select(wantFile && (nodes[wantFile] || wantFile === '__overview__') ? wantFile : '__overview__');
}
if (task) await loadDetail(task, initialFile);
else showEmpty();
}
/* ── router ───────────────────────────────────────── */
function router() {
const raw = location.hash.slice(1) || '/';
const [route, query] = raw.split('?');
const params = new URLSearchParams(query || '');
window.scrollTo(0, 0);
if (route === '/' || route === '' || route === 'home') return renderHome();
if (route === '/datasets' || route === 'datasets') return renderDatasets(params);
if (route === 'dataset' || route === 'task') return renderWorkspace(params);
renderHome();
}
// ⌘K focuses search on datasets page (and jumps there otherwise)
document.addEventListener('keydown', (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
const s = document.getElementById('tp-search') || document.getElementById('ds-search') || document.getElementById('load-input');
if (s) s.focus(); else location.hash = '/datasets';
}
});
// ?dataset= / ?d= prefill (legacy Gradio-style deep link) β†’ dataset view
(function prefill() {
const p = new URLSearchParams(location.search);
const d = p.get('dataset') || p.get('d');
if (d && !location.hash) { location.hash = `dataset?uri=${enc(d)}`; }
})();
window.addEventListener('hashchange', router);
router();