Spaces:
Running
Running
Unify dataset+task into one master-detail workspace; UI polish
Browse files- Merge the dataset task-list view and the task detail view into a single
workspace: tasks side-panel (master) + tree/content (detail), in-place
task switching, empty "select a task" state when none chosen
- Home: curated example bubbles (HF + GitHub) instead of the live table;
no longer leads with a specific dataset
- Datasets tab: example quick-pick chips above the searchable Hub list
- Minimal thin scrollbars; terminal-styled run command; wider layout (1500px)
- Remove the top accent strip; deep-link/badge use the real $SPACE_HOST
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- static/app.js +86 -95
- static/style.css +29 -5
static/app.js
CHANGED
|
@@ -30,6 +30,7 @@ const ICON = {
|
|
| 30 |
next: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>',
|
| 31 |
term: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 17l6-6-6-6M12 19h8"/></svg>',
|
| 32 |
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>',
|
|
|
|
| 33 |
};
|
| 34 |
|
| 35 |
function copyButton(text, cls = 'copy') {
|
|
@@ -45,6 +46,31 @@ function copyButton(text, cls = 'copy') {
|
|
| 45 |
return b;
|
| 46 |
}
|
| 47 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
/* ββ theme ββββββββββββββββββββββββββββββββββββββββββ */
|
| 49 |
function applyTheme(mode) {
|
| 50 |
const sys = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
@@ -118,15 +144,11 @@ async function renderHome() {
|
|
| 118 |
<input id="load-input" placeholder="Load any dataset β owner/name Β· hf:// Β· gh://owner/repo Β· harbor://org/name Β· /local/path" />
|
| 119 |
<span class="kbd">β΅</span>
|
| 120 |
</div>
|
| 121 |
-
<div style="display:flex;align-items:center;justify-content:space-between;margin:
|
| 122 |
-
<h2 style="margin:0">
|
| 123 |
-
<
|
| 124 |
-
</div>
|
| 125 |
-
<div class="card" id="hub-table">
|
| 126 |
-
<div class="thead"><span>Dataset</span><span class="col-tasks">Tasks</span></div>
|
| 127 |
-
<div class="loading"><span class="spinner"></span>fetching from huggingface.co/datasets?other=harbor</div>
|
| 128 |
</div>
|
| 129 |
-
<div
|
| 130 |
|
| 131 |
<div class="howto">
|
| 132 |
<h2>Link your dataset to the visualiser</h2>
|
|
@@ -153,22 +175,11 @@ async function renderHome() {
|
|
| 153 |
`;
|
| 154 |
document.getElementById('copy-link').appendChild(copyButton(`${origin}/?dataset=<owner>/<name>`));
|
| 155 |
document.getElementById('copy-badge').appendChild(copyButton(badgeMd));
|
|
|
|
| 156 |
const input = document.getElementById('load-input');
|
| 157 |
input.addEventListener('keydown', (e) => {
|
| 158 |
if (e.key === 'Enter' && input.value.trim()) location.hash = `dataset?uri=${enc(input.value.trim())}`;
|
| 159 |
});
|
| 160 |
-
|
| 161 |
-
try {
|
| 162 |
-
const { datasets } = await api('/api/hub/datasets?sort=downloads&limit=12');
|
| 163 |
-
const card = document.getElementById('hub-table');
|
| 164 |
-
card.innerHTML = '<div class="thead"><span>Dataset</span><span class="col-tasks">Tasks</span></div>';
|
| 165 |
-
datasets.slice(0, 8).forEach(d => card.appendChild(datasetRow(d.id, null)));
|
| 166 |
-
document.getElementById('hub-status').textContent = `${datasets.length}+ datasets`;
|
| 167 |
-
fillCounts(card);
|
| 168 |
-
} catch (e) {
|
| 169 |
-
document.getElementById('hub-table').innerHTML = `<div class="errbox">Couldn't reach the Hub: ${esc(e.message)}</div>`;
|
| 170 |
-
document.getElementById('hub-status').textContent = '';
|
| 171 |
-
}
|
| 172 |
}
|
| 173 |
|
| 174 |
let _hubCache = null;
|
|
@@ -178,7 +189,8 @@ async function renderDatasets(params) {
|
|
| 178 |
APP.innerHTML = `
|
| 179 |
<div class="page">
|
| 180 |
<h1>Datasets</h1>
|
| 181 |
-
<p class="muted" style="margin:-8px 0
|
|
|
|
| 182 |
<div class="search">
|
| 183 |
${ICON.search}
|
| 184 |
<input id="ds-search" placeholder="Search Harbor datasets on the Hubβ¦" autofocus />
|
|
@@ -195,6 +207,7 @@ async function renderDatasets(params) {
|
|
| 195 |
<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>
|
| 196 |
</div>
|
| 197 |
</div>`;
|
|
|
|
| 198 |
const tbl = document.getElementById('ds-table');
|
| 199 |
const search = document.getElementById('ds-search');
|
| 200 |
const sortSel = document.getElementById('ds-sort'); sortSel.value = sort;
|
|
@@ -225,60 +238,6 @@ async function renderDatasets(params) {
|
|
| 225 |
await load();
|
| 226 |
}
|
| 227 |
|
| 228 |
-
async function renderDataset(params) {
|
| 229 |
-
setActiveNav(null);
|
| 230 |
-
const uri = params.get('uri');
|
| 231 |
-
APP.innerHTML = `
|
| 232 |
-
<div class="page">
|
| 233 |
-
<div class="crumb"><a href="#/datasets">Datasets</a><span class="sep">/</span><span>${esc(uri)}</span></div>
|
| 234 |
-
<div class="loading"><span class="spinner"></span>fetching <b>${esc(uri)}</b> β¦
|
| 235 |
-
<span class="sub">Loading the Harbor spec β this can take a few seconds to a minute for large datasets (the more tasks, the longer the listing).</span>
|
| 236 |
-
</div>
|
| 237 |
-
</div>`;
|
| 238 |
-
let data;
|
| 239 |
-
try { data = await api(`/api/dataset?uri=${enc(uri)}`); }
|
| 240 |
-
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; }
|
| 241 |
-
|
| 242 |
-
const page = APP.querySelector('.page');
|
| 243 |
-
page.innerHTML = `
|
| 244 |
-
<div class="crumb"><a href="#/datasets">Datasets</a><span class="sep">/</span><span>${esc(data.display)}</span>
|
| 245 |
-
<span class="pill">${data.count} tasks</span>
|
| 246 |
-
<button class="btn" id="refresh" style="margin-left:auto;padding:5px 11px;font-size:12px">β» refresh</button>
|
| 247 |
-
</div>
|
| 248 |
-
<div class="search"><span style="color:var(--faint)">${ICON.search}</span>
|
| 249 |
-
<input id="task-search" placeholder="Search ${data.count} tasksβ¦" autofocus /></div>
|
| 250 |
-
<div class="card tasklist" id="tasks"></div>`;
|
| 251 |
-
const tasksCard = document.getElementById('tasks');
|
| 252 |
-
const tsearch = document.getElementById('task-search');
|
| 253 |
-
function draw(list) {
|
| 254 |
-
tasksCard.innerHTML = '<div class="thead"><span>Task</span></div>';
|
| 255 |
-
if (!list.length) { tasksCard.innerHTML += '<div class="empty">no matching tasks</div>'; return; }
|
| 256 |
-
list.slice(0, 500).forEach(tid => {
|
| 257 |
-
const row = document.createElement('div'); row.className = 'row';
|
| 258 |
-
row.onclick = () => { location.hash = `task?${qs({ uri, task: tid })}`; };
|
| 259 |
-
row.innerHTML = `<span class="name">${esc(tid)}</span>`;
|
| 260 |
-
row.appendChild(copyButton(tid));
|
| 261 |
-
tasksCard.appendChild(row);
|
| 262 |
-
});
|
| 263 |
-
if (list.length > 500) tasksCard.innerHTML += `<div class="empty">showing 500 of ${list.length} β refine your search</div>`;
|
| 264 |
-
}
|
| 265 |
-
draw(data.tasks);
|
| 266 |
-
let t;
|
| 267 |
-
tsearch.addEventListener('input', () => {
|
| 268 |
-
clearTimeout(t);
|
| 269 |
-
t = setTimeout(() => {
|
| 270 |
-
const q = tsearch.value.trim().toLowerCase();
|
| 271 |
-
draw(q ? data.tasks.filter(x => x.toLowerCase().includes(q)) : data.tasks);
|
| 272 |
-
}, 100);
|
| 273 |
-
});
|
| 274 |
-
document.getElementById('refresh').onclick = async () => {
|
| 275 |
-
page.querySelector('.crumb').insertAdjacentHTML('beforeend', ' <span class="faint">refreshingβ¦</span>');
|
| 276 |
-
try { const fresh = await api(`/api/dataset?${qs({ uri, refresh: 1 })}`); data.tasks = fresh.tasks; draw(fresh.tasks); }
|
| 277 |
-
catch (e) { alert('refresh failed: ' + e.message); }
|
| 278 |
-
location.reload();
|
| 279 |
-
};
|
| 280 |
-
}
|
| 281 |
-
|
| 282 |
/* ββ task viewer (file tree + content) ββββββββββββββ */
|
| 283 |
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' };
|
| 284 |
function langFor(path) {
|
|
@@ -295,40 +254,49 @@ function harborCmd(kind, ident, taskId) {
|
|
| 295 |
return `huggingface-cli download ${ident} --repo-type dataset --local-dir ${dir} && harbor run -p ${dir} -i ${taskId} -a oracle`;
|
| 296 |
}
|
| 297 |
|
|
|
|
|
|
|
|
|
|
| 298 |
let _taskSiblings = { uri: null, tasks: [], ident: null, kind: null };
|
| 299 |
-
async function
|
| 300 |
-
setActiveNav(
|
| 301 |
const uri = params.get('uri');
|
| 302 |
-
let task = params.get('task');
|
| 303 |
let initialFile = params.get('f');
|
| 304 |
|
| 305 |
-
APP.innerHTML = `<div class="page"><div class="loading"><span class="spinner"></span>loading
|
| 306 |
-
<span class="sub">
|
| 307 |
</div></div>`;
|
| 308 |
|
| 309 |
-
// Sibling task list (
|
| 310 |
// Cached per-uri so flipping between tasks doesn't refetch the list.
|
| 311 |
if (_taskSiblings.uri !== uri) {
|
| 312 |
try {
|
| 313 |
const ds = await api(`/api/dataset?uri=${enc(uri)}`);
|
| 314 |
_taskSiblings = { uri, tasks: ds.tasks || [], ident: ds.ident, kind: ds.kind };
|
| 315 |
-
} catch
|
|
|
|
|
|
|
|
|
|
| 316 |
}
|
| 317 |
const siblings = _taskSiblings.tasks;
|
| 318 |
const ident = _taskSiblings.ident || uri;
|
| 319 |
const kind = _taskSiblings.kind || 'hf';
|
| 320 |
|
| 321 |
const page = APP.querySelector('.page');
|
| 322 |
-
|
|
|
|
| 323 |
page.innerHTML = `
|
| 324 |
<div class="crumb">
|
| 325 |
<button class="nav-btn ghost" id="toggle-tasks" title="Toggle task list">${ICON.panel}</button>
|
| 326 |
-
<a href="#
|
| 327 |
-
<
|
| 328 |
-
<span
|
|
|
|
| 329 |
<span class="pos" id="crumb-pos" style="margin-left:auto"></span>
|
|
|
|
| 330 |
</div>
|
| 331 |
-
<div class="runbar">
|
| 332 |
<span class="lbl">${ICON.term}</span>
|
| 333 |
<code id="run-cmd"></code>
|
| 334 |
<span id="run-copy"></span>
|
|
@@ -336,7 +304,7 @@ async function renderTask(params) {
|
|
| 336 |
<div class="taskview${collapsed ? ' collapsed' : ''}" id="taskview">
|
| 337 |
<div class="tasks-panel" id="tasks-panel">
|
| 338 |
<div class="tp-head">Tasks <span class="faint">${siblings.length}</span></div>
|
| 339 |
-
<div class="tp-search">${ICON.search}<input id="tp-search" placeholder="Filter tasksβ¦" /></div>
|
| 340 |
<div class="tp-list" id="tp-list"></div>
|
| 341 |
</div>
|
| 342 |
<div class="tree" id="tree"></div>
|
|
@@ -347,7 +315,7 @@ async function renderTask(params) {
|
|
| 347 |
const tpList = document.getElementById('tp-list');
|
| 348 |
const tree = document.getElementById('tree');
|
| 349 |
const content = document.getElementById('content');
|
| 350 |
-
const runbar =
|
| 351 |
const runCode = document.getElementById('run-cmd');
|
| 352 |
const runCopyHolder = document.getElementById('run-copy');
|
| 353 |
|
|
@@ -355,6 +323,13 @@ async function renderTask(params) {
|
|
| 355 |
taskview.classList.toggle('collapsed');
|
| 356 |
localStorage.setItem('hh-tasks-collapsed', taskview.classList.contains('collapsed') ? '1' : '0');
|
| 357 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
|
| 359 |
// ββ tasks side panel ββ
|
| 360 |
function drawPanel(filter = '') {
|
|
@@ -383,16 +358,31 @@ async function renderTask(params) {
|
|
| 383 |
const a = tpList.querySelector('.tp-item.active'); if (a) a.scrollIntoView({ block: 'nearest' });
|
| 384 |
}
|
| 385 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 386 |
// ββ load one task's detail into the tree + content (no full re-render) ββ
|
| 387 |
async function loadDetail(tid, wantFile) {
|
| 388 |
task = tid;
|
| 389 |
syncPanelActive(tid);
|
| 390 |
-
document.getElementById('crumb-task').
|
|
|
|
| 391 |
const i = siblings.indexOf(tid);
|
| 392 |
document.getElementById('crumb-pos').textContent = i >= 0 ? `${i + 1} / ${siblings.length}` : '';
|
| 393 |
history.replaceState(null, '', '#' + `task?${qs({ uri, task: tid })}`);
|
| 394 |
|
| 395 |
const cmd = harborCmd(kind, ident, tid);
|
|
|
|
| 396 |
runCode.textContent = cmd;
|
| 397 |
runCopyHolder.innerHTML = '';
|
| 398 |
const rc = copyButton(cmd);
|
|
@@ -406,7 +396,8 @@ async function renderTask(params) {
|
|
| 406 |
catch (e) { content.innerHTML = `<div class="errbox">${esc(e.message)}</div>`; return; }
|
| 407 |
if (task !== tid) return; // a newer click superseded this fetch
|
| 408 |
|
| 409 |
-
document.getElementById('crumb-diff')
|
|
|
|
| 410 |
buildDetail(t, wantFile);
|
| 411 |
}
|
| 412 |
|
|
@@ -479,7 +470,8 @@ async function renderTask(params) {
|
|
| 479 |
select(wantFile && (nodes[wantFile] || wantFile === '__overview__') ? wantFile : '__overview__');
|
| 480 |
}
|
| 481 |
|
| 482 |
-
await loadDetail(task, initialFile);
|
|
|
|
| 483 |
}
|
| 484 |
|
| 485 |
/* ββ router βββββββββββββββββββββββββββββββββββββββββ */
|
|
@@ -490,8 +482,7 @@ function router() {
|
|
| 490 |
window.scrollTo(0, 0);
|
| 491 |
if (route === '/' || route === '' || route === 'home') return renderHome();
|
| 492 |
if (route === '/datasets' || route === 'datasets') return renderDatasets(params);
|
| 493 |
-
if (route === 'dataset') return
|
| 494 |
-
if (route === 'task') return renderTask(params);
|
| 495 |
renderHome();
|
| 496 |
}
|
| 497 |
|
|
@@ -499,7 +490,7 @@ function router() {
|
|
| 499 |
document.addEventListener('keydown', (e) => {
|
| 500 |
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
| 501 |
e.preventDefault();
|
| 502 |
-
const s = document.getElementById('ds-search') || document.getElementById('load-input');
|
| 503 |
if (s) s.focus(); else location.hash = '/datasets';
|
| 504 |
}
|
| 505 |
});
|
|
|
|
| 30 |
next: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>',
|
| 31 |
term: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 17l6-6-6-6M12 19h8"/></svg>',
|
| 32 |
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>',
|
| 33 |
+
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>',
|
| 34 |
};
|
| 35 |
|
| 36 |
function copyButton(text, cls = 'copy') {
|
|
|
|
| 46 |
return b;
|
| 47 |
}
|
| 48 |
|
| 49 |
+
/* ββ curated example datasets (shown as bubbles) ββββ */
|
| 50 |
+
const EXAMPLES = [
|
| 51 |
+
{ label: 'Terminal-Bench 2.0', uri: 'harborframework/terminal-bench-2.0' },
|
| 52 |
+
{ label: 'TaskTrove', uri: 'open-thoughts/TaskTrove' },
|
| 53 |
+
{ label: 'Repo2RLEnv Β· PR diffs', uri: 'AdithyaSK/repo2rlenv-v083-pr_diff' },
|
| 54 |
+
{ label: 'TitanBench', uri: 'billshockley/titanbench' },
|
| 55 |
+
{ label: 'Harbor tasks demo', uri: 'gh://adithya-s-k/harbor-tasks-demo' },
|
| 56 |
+
];
|
| 57 |
+
function srcTag(uri) {
|
| 58 |
+
if (uri.startsWith('gh://') || uri.includes('github.com')) return 'gh';
|
| 59 |
+
if (uri.startsWith('harbor://')) return 'harbor';
|
| 60 |
+
return 'hf';
|
| 61 |
+
}
|
| 62 |
+
function exampleChips() {
|
| 63 |
+
const wrap = document.createElement('div'); wrap.className = 'chips';
|
| 64 |
+
EXAMPLES.forEach(ex => {
|
| 65 |
+
const tag = srcTag(ex.uri);
|
| 66 |
+
const c = document.createElement('button'); c.className = 'chip'; c.title = ex.uri;
|
| 67 |
+
c.innerHTML = `<span class="src ${tag}">${tag}</span><span>${esc(ex.label)}</span>`;
|
| 68 |
+
c.onclick = () => { location.hash = `dataset?uri=${enc(ex.uri)}`; };
|
| 69 |
+
wrap.appendChild(c);
|
| 70 |
+
});
|
| 71 |
+
return wrap;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
/* ββ theme ββββββββββββββββββββββββββββββββββββββββββ */
|
| 75 |
function applyTheme(mode) {
|
| 76 |
const sys = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
|
|
| 144 |
<input id="load-input" placeholder="Load any dataset β owner/name Β· hf:// Β· gh://owner/repo Β· harbor://org/name Β· /local/path" />
|
| 145 |
<span class="kbd">β΅</span>
|
| 146 |
</div>
|
| 147 |
+
<div style="display:flex;align-items:center;justify-content:space-between;margin:30px 0 14px">
|
| 148 |
+
<h2 style="margin:0">Examples</h2>
|
| 149 |
+
<a class="faint hl" href="#/datasets" style="font-size:13px">Browse all Harbor datasets β</a>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
</div>
|
| 151 |
+
<div id="examples"></div>
|
| 152 |
|
| 153 |
<div class="howto">
|
| 154 |
<h2>Link your dataset to the visualiser</h2>
|
|
|
|
| 175 |
`;
|
| 176 |
document.getElementById('copy-link').appendChild(copyButton(`${origin}/?dataset=<owner>/<name>`));
|
| 177 |
document.getElementById('copy-badge').appendChild(copyButton(badgeMd));
|
| 178 |
+
document.getElementById('examples').appendChild(exampleChips());
|
| 179 |
const input = document.getElementById('load-input');
|
| 180 |
input.addEventListener('keydown', (e) => {
|
| 181 |
if (e.key === 'Enter' && input.value.trim()) location.hash = `dataset?uri=${enc(input.value.trim())}`;
|
| 182 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
}
|
| 184 |
|
| 185 |
let _hubCache = null;
|
|
|
|
| 189 |
APP.innerHTML = `
|
| 190 |
<div class="page">
|
| 191 |
<h1>Datasets</h1>
|
| 192 |
+
<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>
|
| 193 |
+
<div id="ds-examples" style="margin-bottom:20px"></div>
|
| 194 |
<div class="search">
|
| 195 |
${ICON.search}
|
| 196 |
<input id="ds-search" placeholder="Search Harbor datasets on the Hubβ¦" autofocus />
|
|
|
|
| 207 |
<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>
|
| 208 |
</div>
|
| 209 |
</div>`;
|
| 210 |
+
document.getElementById('ds-examples').appendChild(exampleChips());
|
| 211 |
const tbl = document.getElementById('ds-table');
|
| 212 |
const search = document.getElementById('ds-search');
|
| 213 |
const sortSel = document.getElementById('ds-sort'); sortSel.value = sort;
|
|
|
|
| 238 |
await load();
|
| 239 |
}
|
| 240 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
/* ββ task viewer (file tree + content) ββββββββββββββ */
|
| 242 |
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' };
|
| 243 |
function langFor(path) {
|
|
|
|
| 254 |
return `huggingface-cli download ${ident} --repo-type dataset --local-dir ${dir} && harbor run -p ${dir} -i ${taskId} -a oracle`;
|
| 255 |
}
|
| 256 |
|
| 257 |
+
// Unified dataset+task workspace: tasks side-panel (master) + file tree + content (detail).
|
| 258 |
+
// Used by BOTH the `dataset?uri=` route (no task β empty detail) and the
|
| 259 |
+
// `task?uri=&task=` route (task preselected). Switching tasks happens in place.
|
| 260 |
let _taskSiblings = { uri: null, tasks: [], ident: null, kind: null };
|
| 261 |
+
async function renderWorkspace(params) {
|
| 262 |
+
setActiveNav('datasets');
|
| 263 |
const uri = params.get('uri');
|
| 264 |
+
let task = params.get('task') || null;
|
| 265 |
let initialFile = params.get('f');
|
| 266 |
|
| 267 |
+
APP.innerHTML = `<div class="page"><div class="loading"><span class="spinner"></span>loadingβ¦
|
| 268 |
+
<span class="sub">Loading the Harbor spec β a few seconds to a minute for large datasets (the more tasks, the longer the listing).</span>
|
| 269 |
</div></div>`;
|
| 270 |
|
| 271 |
+
// Sibling task list (the panel) + canonical ident/kind (run command).
|
| 272 |
// Cached per-uri so flipping between tasks doesn't refetch the list.
|
| 273 |
if (_taskSiblings.uri !== uri) {
|
| 274 |
try {
|
| 275 |
const ds = await api(`/api/dataset?uri=${enc(uri)}`);
|
| 276 |
_taskSiblings = { uri, tasks: ds.tasks || [], ident: ds.ident, kind: ds.kind };
|
| 277 |
+
} catch (e) {
|
| 278 |
+
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>`;
|
| 279 |
+
return;
|
| 280 |
+
}
|
| 281 |
}
|
| 282 |
const siblings = _taskSiblings.tasks;
|
| 283 |
const ident = _taskSiblings.ident || uri;
|
| 284 |
const kind = _taskSiblings.kind || 'hf';
|
| 285 |
|
| 286 |
const page = APP.querySelector('.page');
|
| 287 |
+
// Honour the collapse pref only when a task is open; with no task the panel is the focus.
|
| 288 |
+
const collapsed = task && localStorage.getItem('hh-tasks-collapsed') === '1';
|
| 289 |
page.innerHTML = `
|
| 290 |
<div class="crumb">
|
| 291 |
<button class="nav-btn ghost" id="toggle-tasks" title="Toggle task list">${ICON.panel}</button>
|
| 292 |
+
<a href="#/datasets">Datasets</a><span class="sep">/</span>
|
| 293 |
+
<a href="#dataset?uri=${enc(uri)}" id="crumb-ident">${esc(ident)}</a>
|
| 294 |
+
<span class="pill">${siblings.length} tasks</span>
|
| 295 |
+
<span id="crumb-task-wrap"></span>
|
| 296 |
<span class="pos" id="crumb-pos" style="margin-left:auto"></span>
|
| 297 |
+
<button class="nav-btn ghost" id="ws-refresh" title="Refresh dataset">${ICON.refresh}</button>
|
| 298 |
</div>
|
| 299 |
+
<div class="runbar" id="runbar" hidden>
|
| 300 |
<span class="lbl">${ICON.term}</span>
|
| 301 |
<code id="run-cmd"></code>
|
| 302 |
<span id="run-copy"></span>
|
|
|
|
| 304 |
<div class="taskview${collapsed ? ' collapsed' : ''}" id="taskview">
|
| 305 |
<div class="tasks-panel" id="tasks-panel">
|
| 306 |
<div class="tp-head">Tasks <span class="faint">${siblings.length}</span></div>
|
| 307 |
+
<div class="tp-search">${ICON.search}<input id="tp-search" placeholder="Filter tasksβ¦" autofocus /></div>
|
| 308 |
<div class="tp-list" id="tp-list"></div>
|
| 309 |
</div>
|
| 310 |
<div class="tree" id="tree"></div>
|
|
|
|
| 315 |
const tpList = document.getElementById('tp-list');
|
| 316 |
const tree = document.getElementById('tree');
|
| 317 |
const content = document.getElementById('content');
|
| 318 |
+
const runbar = document.getElementById('runbar');
|
| 319 |
const runCode = document.getElementById('run-cmd');
|
| 320 |
const runCopyHolder = document.getElementById('run-copy');
|
| 321 |
|
|
|
|
| 323 |
taskview.classList.toggle('collapsed');
|
| 324 |
localStorage.setItem('hh-tasks-collapsed', taskview.classList.contains('collapsed') ? '1' : '0');
|
| 325 |
};
|
| 326 |
+
document.getElementById('ws-refresh').onclick = async () => {
|
| 327 |
+
try {
|
| 328 |
+
const ds = await api(`/api/dataset?${qs({ uri, refresh: 1 })}`);
|
| 329 |
+
_taskSiblings = { uri, tasks: ds.tasks || [], ident: ds.ident, kind: ds.kind };
|
| 330 |
+
} catch (e) { alert('refresh failed: ' + e.message); return; }
|
| 331 |
+
renderWorkspace(params);
|
| 332 |
+
};
|
| 333 |
|
| 334 |
// ββ tasks side panel ββ
|
| 335 |
function drawPanel(filter = '') {
|
|
|
|
| 358 |
const a = tpList.querySelector('.tp-item.active'); if (a) a.scrollIntoView({ block: 'nearest' });
|
| 359 |
}
|
| 360 |
|
| 361 |
+
// Empty (no task selected) β master-detail "nothing selected" state.
|
| 362 |
+
function showEmpty() {
|
| 363 |
+
task = null;
|
| 364 |
+
runbar.hidden = true;
|
| 365 |
+
syncPanelActive(null);
|
| 366 |
+
document.getElementById('crumb-task-wrap').innerHTML = '';
|
| 367 |
+
document.getElementById('crumb-pos').textContent = '';
|
| 368 |
+
history.replaceState(null, '', '#' + `dataset?${qs({ uri })}`);
|
| 369 |
+
tree.innerHTML = '';
|
| 370 |
+
content.innerHTML = `<div class="emptysel"><div class="ic">${ICON.panel}</div>
|
| 371 |
+
<p>Select a task from the list to view its spec, files & run command.</p></div>`;
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
// ββ load one task's detail into the tree + content (no full re-render) ββ
|
| 375 |
async function loadDetail(tid, wantFile) {
|
| 376 |
task = tid;
|
| 377 |
syncPanelActive(tid);
|
| 378 |
+
document.getElementById('crumb-task-wrap').innerHTML =
|
| 379 |
+
`<span class="sep">/</span><span id="crumb-task">${esc(tid)}</span><span id="crumb-diff"></span>`;
|
| 380 |
const i = siblings.indexOf(tid);
|
| 381 |
document.getElementById('crumb-pos').textContent = i >= 0 ? `${i + 1} / ${siblings.length}` : '';
|
| 382 |
history.replaceState(null, '', '#' + `task?${qs({ uri, task: tid })}`);
|
| 383 |
|
| 384 |
const cmd = harborCmd(kind, ident, tid);
|
| 385 |
+
runbar.hidden = false;
|
| 386 |
runCode.textContent = cmd;
|
| 387 |
runCopyHolder.innerHTML = '';
|
| 388 |
const rc = copyButton(cmd);
|
|
|
|
| 396 |
catch (e) { content.innerHTML = `<div class="errbox">${esc(e.message)}</div>`; return; }
|
| 397 |
if (task !== tid) return; // a newer click superseded this fetch
|
| 398 |
|
| 399 |
+
const diffEl = document.getElementById('crumb-diff');
|
| 400 |
+
if (diffEl) diffEl.innerHTML = t.difficulty ? ` <span class="pill">${esc(t.difficulty)}</span>` : '';
|
| 401 |
buildDetail(t, wantFile);
|
| 402 |
}
|
| 403 |
|
|
|
|
| 470 |
select(wantFile && (nodes[wantFile] || wantFile === '__overview__') ? wantFile : '__overview__');
|
| 471 |
}
|
| 472 |
|
| 473 |
+
if (task) await loadDetail(task, initialFile);
|
| 474 |
+
else showEmpty();
|
| 475 |
}
|
| 476 |
|
| 477 |
/* ββ router βββββββββββββββββββββββββββββββββββββββββ */
|
|
|
|
| 482 |
window.scrollTo(0, 0);
|
| 483 |
if (route === '/' || route === '' || route === 'home') return renderHome();
|
| 484 |
if (route === '/datasets' || route === 'datasets') return renderDatasets(params);
|
| 485 |
+
if (route === 'dataset' || route === 'task') return renderWorkspace(params);
|
|
|
|
| 486 |
renderHome();
|
| 487 |
}
|
| 488 |
|
|
|
|
| 490 |
document.addEventListener('keydown', (e) => {
|
| 491 |
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
| 492 |
e.preventDefault();
|
| 493 |
+
const s = document.getElementById('tp-search') || document.getElementById('ds-search') || document.getElementById('load-input');
|
| 494 |
if (s) s.focus(); else location.hash = '/datasets';
|
| 495 |
}
|
| 496 |
});
|
static/style.css
CHANGED
|
@@ -8,7 +8,7 @@
|
|
| 8 |
--ok: #16a34a; --warn: #d97706; --err: #dc2626;
|
| 9 |
--hover: #f1f3f5;
|
| 10 |
--mono: 'JetBrains Mono', ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace;
|
| 11 |
-
--radius: 10px; --nav-h: 56px; --maxw:
|
| 12 |
}
|
| 13 |
:root[data-theme="dark"] {
|
| 14 |
--bg: #0b0d12; --panel: #11141b; --panel-2: #1a1e27;
|
|
@@ -21,6 +21,14 @@
|
|
| 21 |
}
|
| 22 |
* { box-sizing: border-box; }
|
| 23 |
html, body { margin: 0; padding: 0; overflow-x: hidden; max-width: 100%; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
body {
|
| 25 |
background: var(--bg); color: var(--text);
|
| 26 |
font-family: var(--mono); font-size: 14px; line-height: 1.55;
|
|
@@ -64,10 +72,6 @@ h2 { font-size: 18px; font-weight: 600; margin: 0 0 12px; }
|
|
| 64 |
.muted { color: var(--muted); }
|
| 65 |
.faint { color: var(--faint); }
|
| 66 |
|
| 67 |
-
/* thin Hugging Face accent strip at the very top */
|
| 68 |
-
body::before { content: ""; display: block; height: 3px;
|
| 69 |
-
background: linear-gradient(90deg, var(--hf-yellow), var(--hf-orange)); }
|
| 70 |
-
|
| 71 |
/* hero */
|
| 72 |
.hero { text-align: center; padding: 60px 0 30px; }
|
| 73 |
.hero .mark { font-size: 56px; line-height: 1; margin-bottom: 10px; }
|
|
@@ -76,6 +80,19 @@ body::before { content: ""; display: block; height: 3px;
|
|
| 76 |
-webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }
|
| 77 |
.hero p { color: var(--muted); font-size: 14.5px; margin: 0 auto; max-width: 640px; line-height: 1.6; }
|
| 78 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
/* how-to / embed instructions */
|
| 80 |
.howto { margin: 46px 0 0; }
|
| 81 |
.howto h2 { font-size: 15px; margin: 0 0 14px; }
|
|
@@ -187,6 +204,13 @@ body::before { content: ""; display: block; height: 3px;
|
|
| 187 |
.tp-item.active { background: var(--accent-soft); color: var(--text); border-left-color: var(--hf-orange); }
|
| 188 |
.tp-list .empty { padding: 14px; font-size: 11.5px; color: var(--faint); }
|
| 189 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
.content { overflow: auto; max-height: 80vh; background: var(--bg); }
|
| 191 |
.taskview .tree, .taskview .content { max-height: none; }
|
| 192 |
.content .fhead { display: flex; align-items: center; gap: 10px; padding: 10px 16px;
|
|
|
|
| 8 |
--ok: #16a34a; --warn: #d97706; --err: #dc2626;
|
| 9 |
--hover: #f1f3f5;
|
| 10 |
--mono: 'JetBrains Mono', ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace;
|
| 11 |
+
--radius: 10px; --nav-h: 56px; --maxw: 1500px;
|
| 12 |
}
|
| 13 |
:root[data-theme="dark"] {
|
| 14 |
--bg: #0b0d12; --panel: #11141b; --panel-2: #1a1e27;
|
|
|
|
| 21 |
}
|
| 22 |
* { box-sizing: border-box; }
|
| 23 |
html, body { margin: 0; padding: 0; overflow-x: hidden; max-width: 100%; }
|
| 24 |
+
|
| 25 |
+
/* minimal scrollbars everywhere */
|
| 26 |
+
* { scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; }
|
| 27 |
+
::-webkit-scrollbar { width: 7px; height: 7px; }
|
| 28 |
+
::-webkit-scrollbar-track { background: transparent; }
|
| 29 |
+
::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 8px; border: 2px solid transparent; background-clip: padding-box; }
|
| 30 |
+
::-webkit-scrollbar-thumb:hover { background: var(--faint); background-clip: padding-box; }
|
| 31 |
+
::-webkit-scrollbar-corner { background: transparent; }
|
| 32 |
body {
|
| 33 |
background: var(--bg); color: var(--text);
|
| 34 |
font-family: var(--mono); font-size: 14px; line-height: 1.55;
|
|
|
|
| 72 |
.muted { color: var(--muted); }
|
| 73 |
.faint { color: var(--faint); }
|
| 74 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
/* hero */
|
| 76 |
.hero { text-align: center; padding: 60px 0 30px; }
|
| 77 |
.hero .mark { font-size: 56px; line-height: 1; margin-bottom: 10px; }
|
|
|
|
| 80 |
-webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }
|
| 81 |
.hero p { color: var(--muted); font-size: 14.5px; margin: 0 auto; max-width: 640px; line-height: 1.6; }
|
| 82 |
|
| 83 |
+
/* example bubbles / quick-pick chips */
|
| 84 |
+
.chips { display: flex; flex-wrap: wrap; gap: 10px; }
|
| 85 |
+
.chip { display: inline-flex; align-items: center; gap: 9px; padding: 9px 14px 9px 11px; border-radius: 999px;
|
| 86 |
+
border: 1px solid var(--border); background: var(--panel); color: var(--text); font-size: 13px;
|
| 87 |
+
cursor: pointer; transition: border-color .12s, background .12s, transform .08s; }
|
| 88 |
+
.chip:hover { border-color: var(--hf-orange); background: var(--hover); }
|
| 89 |
+
.chip:active { transform: translateY(1px); }
|
| 90 |
+
.chip .src { font-size: 9px; font-weight: 700; letter-spacing: .5px; text-transform: uppercase;
|
| 91 |
+
padding: 2px 6px; border-radius: 5px; flex: none; }
|
| 92 |
+
.chip .src.hf { background: color-mix(in srgb, var(--hf-yellow) 22%, var(--panel-2)); color: var(--hf-orange); }
|
| 93 |
+
.chip .src.gh { background: var(--panel-2); color: var(--muted); }
|
| 94 |
+
.chip .src.harbor { background: var(--accent-soft); color: var(--accent); }
|
| 95 |
+
|
| 96 |
/* how-to / embed instructions */
|
| 97 |
.howto { margin: 46px 0 0; }
|
| 98 |
.howto h2 { font-size: 15px; margin: 0 0 14px; }
|
|
|
|
| 204 |
.tp-item.active { background: var(--accent-soft); color: var(--text); border-left-color: var(--hf-orange); }
|
| 205 |
.tp-list .empty { padding: 14px; font-size: 11.5px; color: var(--faint); }
|
| 206 |
|
| 207 |
+
/* nothing-selected state in the detail pane */
|
| 208 |
+
.emptysel { display: flex; flex-direction: column; align-items: center; justify-content: center;
|
| 209 |
+
gap: 14px; height: 100%; min-height: 320px; padding: 40px; text-align: center; color: var(--faint); }
|
| 210 |
+
.emptysel .ic { color: var(--border-strong); line-height: 0; }
|
| 211 |
+
.emptysel .ic svg { width: 44px; height: 44px; }
|
| 212 |
+
.emptysel p { margin: 0; font-size: 13px; max-width: 320px; line-height: 1.6; }
|
| 213 |
+
|
| 214 |
.content { overflow: auto; max-height: 80vh; background: var(--bg); }
|
| 215 |
.taskview .tree, .taskview .content { max-height: none; }
|
| 216 |
.content .fhead { display: flex; align-items: center; gap: 10px; padding: 10px 16px;
|