/* 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: 'Repo2RLEnv · PR diffs', uri: 'AdithyaSK/repo2rlenv-v083-pr_diff' },
{ label: 'TitanBench', uri: 'billshockley/titanbench' },
{ label: 'DABstep · Harbor', uri: 'AdithyaSK/dabstep-harbor' },
{ 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 = `[](${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.
${ICON.search}
↵
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 Visualiser View 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.
${ICON.search}
Most downloads
Most likes
Recently updated
⌘K
${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 = 'Dataset Tasks
';
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 = `
${ICON.term}
Tasks ${siblings.length}
${ICON.search}
`;
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 = siblings.length
? `${ICON.panel}
Select a task from the list to view its spec, files & run command.
`
: `${ICON.info}
No Harbor tasks found in this dataset.
The visualiser recognises Harbor datasets by either a registry.json at the root, a tasks/ folder (nested layout), or top-level dirs containing task.toml (flat layout). This dataset doesn't follow any of those.
`;
}
// ── 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 += `${esc(k)} ${esc(v)} `);
if (t.keywords && t.keywords.length) html += `Keywords ${t.keywords.map(k => `${esc(k)} `).join('')} `;
if (t.repo2env) html += `repo2env ${esc(JSON.stringify(t.repo2env, null, 2))} `;
html += '
';
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();