Spaces:
Running
Running
| /* Harbor Hub β SPA frontend. Vanilla JS, hash-routed, talks to the FastAPI API. */ | |
| ; | |
| const APP = document.getElementById('app'); | |
| /* ββ tiny helpers βββββββββββββββββββββββββββββββββββ */ | |
| const esc = (s) => String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); | |
| 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 = `[](${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 β</a> task-spec datasets <strong style="color:var(--text)">straight from the Hugging Face Hub</strong> β metadata, instructions, oracle patches, tests & 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=<owner>/<name></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=<owner>/<name></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 & 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 Face 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(); | |