/* 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, '&').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: '', check: '', search: '', file: '', dir: '', info: '', back: '', next: '', term: '', panel: '', refresh: '', }; 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 = `${tag}${esc(ex.label)}`; 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 = '···'; 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 = `
🤗

Hugging Face Harbor Visualiser

Visualise Harbor ↗ task-spec datasets straight from the Hugging Face Hub — metadata, instructions, oracle patches, tests & Dockerfiles. Also works with GitHub repos and local paths. No bulk download, always the latest.

Examples

Browse all Harbor datasets →

Link your dataset to the visualiser

Deep-link any dataset

Append ?dataset=<owner>/<name> to open straight into a dataset's tasks — handy from a dataset card or docs.

${esc(origin)}/?dataset=<owner>/<name>

Add a badge to your dataset card

Paste this Markdown into your dataset README so a 🤗 badge always links here:

🤗 Harbor VisualiserView Tasks
${esc(badgeMd)}
`; document.getElementById('copy-link').appendChild(copyButton(`${origin}/?dataset=/`)); 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 = `

Datasets

Search across every Harbor-tagged dataset on the Hugging Face Hub — the live other=harbor filter.

loading…
${ICON.info} Want your dataset to show up here? Add the harbor tag to your dataset card's metadata (tags: [harbor] in the README front-matter) and it'll appear in this list automatically.
`; 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 = '
loading…
'; try { const { datasets } = await api(`/api/hub/datasets?${qs({ sort: sortSel.value, limit: 1000 })}`); _hubCache = datasets; draw(datasets); } catch (e) { tbl.innerHTML = `
${esc(e.message)}
`; } } function draw(list) { tbl.innerHTML = '
DatasetTasks
'; if (!list.length) { tbl.innerHTML += '
no matching datasets
'; return; } list.slice(0, 300).forEach(d => tbl.appendChild(datasetRow(d.id, null))); if (list.length > 300) tbl.innerHTML += `
showing 300 of ${list.length} — refine your search
`; 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. // Uses the new `hf` CLI (huggingface_hub 1.x; `huggingface-cli` is deprecated). const dir = ident.split('/').pop(); return `hf 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 = `
loading… Loading the Harbor spec — a few seconds to a minute for large datasets (the more tasks, the longer the listing).
`; // 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 = `
Failed to load ${esc(uri)}: ${esc(e.message)}
`; 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 = `
Datasets/ ${esc(ident)} ${siblings.length} tasks
Tasks ${siblings.length}
`; 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 = `
${ICON.panel}

Select a task from the list to view its spec, files & run command.

`; } // ── 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 = `/${esc(tid)}`; 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 = `
loading task…
`; let t; try { t = await api(`/api/task?${qs({ uri, task: tid })}`); } catch (e) { content.innerHTML = `
${esc(e.message)}
`; return; } if (task !== tid) return; // a newer click superseded this fetch const diffEl = document.getElementById('crumb-diff'); if (diffEl) diffEl.innerHTML = t.difficulty ? ` ${esc(t.difficulty)}` : ''; buildDetail(t, wantFile); } function buildDetail(t, wantFile) { const files = t.files || {}; const paths = Object.keys(files).sort(); tree.innerHTML = `
${esc(t.id)}
`; 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) + `${esc(label)}`; 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 = `
${ICON.info} Overview
`; if (t.description) html += `
${marked.parse(t.description)}
`; html += ''; rows.forEach(([k, v]) => html += ``); if (t.keywords && t.keywords.length) html += ``; if (t.repo2env) html += ``; html += '
${esc(k)}${esc(v)}
Keywords${t.keywords.map(k => `${esc(k)}`).join('')}
repo2env
${esc(JSON.stringify(t.repo2env, null, 2))}
'; const instr = files['instruction.md'] || t.instruction_inline; if (instr) html += `
${ICON.file} instruction.md
${marked.parse(instr)}
`; 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 = `${ICON.file} ${esc(path)}`; 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();