/* 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 = `[](${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.
Search across every Harbor-tagged dataset on the Hugging Face Hub — the live other=harbor filter.
${ICON.search}
⌘K
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.