Spaces:
Running
Running
| (function () { | |
| "use strict"; | |
| let MANIFEST = null; | |
| const PAGE_CACHE = {}; | |
| const UNFURL_CACHE = {}; | |
| function esc(s) { | |
| return String(s) | |
| .replace(/&/g, "&") | |
| .replace(/</g, "<") | |
| .replace(/>/g, ">"); | |
| } | |
| function flattenTree(node, depth, acc) { | |
| acc.push({ node: node, depth: depth }); | |
| (node.children || []).forEach((c) => flattenTree(c, depth + 1, acc)); | |
| return acc; | |
| } | |
| function findNode(node, slug) { | |
| if (node.slug === slug) return node; | |
| for (const c of node.children || []) { | |
| const hit = findNode(c, slug); | |
| if (hit) return hit; | |
| } | |
| return null; | |
| } | |
| /* -------------------- minimal markdown -------------------- */ | |
| function inline(text) { | |
| let t = esc(text); | |
| t = t.replace(/`([^`]+)`/g, (_, c) => `<code>${c}</code>`); | |
| t = t.replace(/\*\*([^*]+)\*\*/g, (_, c) => `<strong>${c}</strong>`); | |
| t = t.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, txt, url) => { | |
| const safe = esc(url); | |
| const attrs = /^https?:/.test(url) ? ' target="_blank" rel="noopener"' : ""; | |
| return `<a href="${safe}"${attrs}>${txt}</a>`; | |
| }); | |
| t = t.replace(/(^|[\s(])(https?:\/\/[^\s<)]+)/g, (m, pre, url) => { | |
| return `${pre}<a href="${url}" target="_blank" rel="noopener">${url}</a>`; | |
| }); | |
| return t; | |
| } | |
| const URL_ONLY = /^(https?:\/\/[^\s]+)$/; | |
| function renderMarkdown(md, container) { | |
| const lines = md.replace(/<!--[\s\S]*?-->/g, "").split("\n"); | |
| let i = 0; | |
| let para = []; | |
| function flushPara() { | |
| if (!para.length) return; | |
| const joined = para.join(" ").trim(); | |
| para = []; | |
| if (!joined) return; | |
| if (URL_ONLY.test(joined) || IMG_PATH.test(joined)) { | |
| container.appendChild(unfurl(joined)); | |
| return; | |
| } | |
| const p = document.createElement("p"); | |
| p.innerHTML = inline(joined); | |
| container.appendChild(p); | |
| } | |
| while (i < lines.length) { | |
| const line = lines[i]; | |
| const trimmed = line.trim(); | |
| if (trimmed === "") { | |
| flushPara(); | |
| i++; | |
| continue; | |
| } | |
| const fence = trimmed.match(/^(`{3,}|~{3,})(.*)$/); | |
| if (fence) { | |
| flushPara(); | |
| const marker = fence[1][0]; | |
| const closeRe = new RegExp("^" + marker + "{" + fence[1].length + ",}\\s*$"); | |
| const info = fence[2].trim(); | |
| const buf = []; | |
| i++; | |
| while (i < lines.length && !closeRe.test(lines[i].trim())) { | |
| buf.push(lines[i]); | |
| i++; | |
| } | |
| i++; | |
| const lang = (info.split(/\s+/)[0] || "").toLowerCase(); | |
| const tm = info.match(/title=(\S+)/); | |
| container.appendChild( | |
| renderCode(buf.join("\n"), lang, tm ? tm[1] : null) | |
| ); | |
| continue; | |
| } | |
| if (trimmed === "---") { | |
| flushPara(); | |
| container.appendChild(document.createElement("hr")); | |
| i++; | |
| continue; | |
| } | |
| const h = trimmed.match(/^(#{1,4})\s+(.*)$/); | |
| if (h) { | |
| flushPara(); | |
| const el = document.createElement("h" + h[1].length); | |
| el.innerHTML = inline(h[2]); | |
| container.appendChild(el); | |
| i++; | |
| continue; | |
| } | |
| if ( | |
| trimmed.startsWith("|") && | |
| i + 1 < lines.length && | |
| /^\|?[\s:|-]*-{2,}[\s:|-]*\|?$/.test(lines[i + 1].trim()) | |
| ) { | |
| flushPara(); | |
| const rows = []; | |
| while (i < lines.length && lines[i].trim().startsWith("|")) { | |
| rows.push(parseRow(lines[i].trim())); | |
| i++; | |
| } | |
| renderTable(rows, container); | |
| continue; | |
| } | |
| if (trimmed.startsWith("> ")) { | |
| flushPara(); | |
| const bq = document.createElement("blockquote"); | |
| bq.innerHTML = inline(trimmed.slice(2)); | |
| container.appendChild(bq); | |
| i++; | |
| continue; | |
| } | |
| if (/^`[^`]+`$/.test(trimmed)) { | |
| flushPara(); | |
| const el = document.createElement("div"); | |
| el.className = "ts"; | |
| el.textContent = trimmed.replace(/`/g, ""); | |
| container.appendChild(el); | |
| i++; | |
| continue; | |
| } | |
| if (trimmed.startsWith("- ")) { | |
| flushPara(); | |
| const items = []; | |
| while (i < lines.length && lines[i].trim().startsWith("- ")) { | |
| items.push(lines[i].trim().slice(2).trim()); | |
| i++; | |
| } | |
| renderList(items, container); | |
| continue; | |
| } | |
| para.push(trimmed); | |
| i++; | |
| } | |
| flushPara(); | |
| } | |
| function parseRow(line) { | |
| let s = line.trim(); | |
| if (s.startsWith("|")) s = s.slice(1); | |
| if (s.endsWith("|")) s = s.slice(0, -1); | |
| return s.split(/(?<!\\)\|/).map((c) => c.replace(/\\\|/g, "|").trim()); | |
| } | |
| const TRUTHY = ["x", "✓", "✔", "yes", "done", "true", "[x]"]; | |
| const CHIP_COLORS = [ | |
| ["#e7f0ff", "#2158d0"], | |
| ["#fde8ec", "#c62a4b"], | |
| ["#e6f7ee", "#1a8a55"], | |
| ["#fdf0e0", "#b26a12"], | |
| ["#efe9ff", "#5b3bd6"], | |
| ["#e6f6f8", "#127b88"], | |
| ]; | |
| function chipColor(name) { | |
| let h = 0; | |
| for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) >>> 0; | |
| return CHIP_COLORS[h % CHIP_COLORS.length]; | |
| } | |
| const STATUS_MAP = { | |
| "": ["Planned", "gray"], | |
| planned: ["Planned", "gray"], | |
| todo: ["Planned", "gray"], | |
| "to do": ["Planned", "gray"], | |
| backlog: ["Planned", "gray"], | |
| "in progress": ["In progress", "amber"], | |
| "in-progress": ["In progress", "amber"], | |
| wip: ["In progress", "amber"], | |
| running: ["In progress", "amber"], | |
| active: ["In progress", "amber"], | |
| done: ["Done", "green"], | |
| complete: ["Done", "green"], | |
| completed: ["Done", "green"], | |
| blocked: ["Blocked", "red"], | |
| failed: ["Failed", "red"], | |
| abandoned: ["Abandoned", "gray"], | |
| }; | |
| function statusBadge(val) { | |
| const [label, tone] = STATUS_MAP[val.toLowerCase()] || [val || "—", "gray"]; | |
| return `<span class="badge ${tone}">${esc(label)}</span>`; | |
| } | |
| function renderTable(rows, container) { | |
| if (rows.length < 2) return; | |
| const header = rows[0]; | |
| const body = rows.slice(2); | |
| const roles = header.map((h) => { | |
| const t = h.toLowerCase(); | |
| if (t.includes("status") || t.includes("state")) return "status"; | |
| if (t.includes("progress") || t.includes("complete") || t.includes("done")) | |
| return "check"; | |
| if (t === "who" || t.includes("assign") || t.includes("owner")) return "who"; | |
| return "text"; | |
| }); | |
| const table = document.createElement("table"); | |
| table.className = "board"; | |
| const thead = document.createElement("thead"); | |
| const htr = document.createElement("tr"); | |
| header.forEach((h, c) => { | |
| const th = document.createElement("th"); | |
| th.textContent = h; | |
| if (roles[c] === "check") th.className = "col-check"; | |
| htr.appendChild(th); | |
| }); | |
| thead.appendChild(htr); | |
| table.appendChild(thead); | |
| const tbody = document.createElement("tbody"); | |
| body.forEach((cells) => { | |
| const nonEmpty = cells.filter((x) => x !== "").length; | |
| if (nonEmpty === 1 && cells[0]) { | |
| const tr = document.createElement("tr"); | |
| tr.className = "section-row"; | |
| const td = document.createElement("td"); | |
| td.colSpan = header.length; | |
| td.innerHTML = inline(cells[0]); | |
| tr.appendChild(td); | |
| tbody.appendChild(tr); | |
| return; | |
| } | |
| const tr = document.createElement("tr"); | |
| header.forEach((_, c) => { | |
| const td = document.createElement("td"); | |
| const val = (cells[c] || "").trim(); | |
| if (roles[c] === "status") { | |
| td.className = "col-status"; | |
| td.innerHTML = statusBadge(val); | |
| } else if (roles[c] === "check") { | |
| td.className = "col-check"; | |
| const on = TRUTHY.indexOf(val.toLowerCase()) !== -1; | |
| td.innerHTML = `<span class="box ${on ? "on" : ""}">${on ? "✓" : ""}</span>`; | |
| } else if (roles[c] === "who") { | |
| if (!val || /^to assign$/i.test(val)) { | |
| td.innerHTML = `<span class="who-chip muted">${esc(val || "—")}</span>`; | |
| } else { | |
| const [bg, fg] = chipColor(val); | |
| td.innerHTML = `<span class="who-chip" style="background:${bg};color:${fg}">${esc(val)}</span>`; | |
| } | |
| } else { | |
| td.innerHTML = inline(val); | |
| } | |
| tr.appendChild(td); | |
| }); | |
| const link = tr.querySelector('a[href^="#/"]'); | |
| if (link) { | |
| tr.classList.add("linked-row"); | |
| tr.addEventListener("click", (e) => { | |
| if (e.target.tagName !== "A") location.hash = link.getAttribute("href"); | |
| }); | |
| } | |
| tbody.appendChild(tr); | |
| }); | |
| table.appendChild(tbody); | |
| const wrap = document.createElement("div"); | |
| wrap.className = "board-wrap"; | |
| wrap.appendChild(table); | |
| container.appendChild(wrap); | |
| } | |
| const HL_RULES = { | |
| python: [ | |
| ["comment", /#[^\n]*/], | |
| ["string", /'''[\s\S]*?'''|"""[\s\S]*?"""|'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*"/], | |
| [ | |
| "keyword", | |
| /\b(?:def|class|return|if|elif|else|for|while|import|from|as|with|try|except|finally|raise|in|not|and|or|is|None|True|False|lambda|yield|global|nonlocal|assert|pass|break|continue|async|await|print)\b/, | |
| ], | |
| ["number", /\b\d[\d_.eE+-]*\b/], | |
| ], | |
| bash: [ | |
| ["comment", /#[^\n]*/], | |
| ["string", /'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*"/], | |
| ["keyword", /\b(?:if|then|else|fi|for|in|do|done|while|case|esac|function|export|source|echo|cd|return|local)\b/], | |
| ["number", /(?<=\s)-{1,2}[a-zA-Z][\w-]*/], | |
| ], | |
| json: [ | |
| ["string", /"(?:\\.|[^"\\])*"/], | |
| ["keyword", /\b(?:true|false|null)\b/], | |
| ["number", /-?\b\d[\d.eE+-]*\b/], | |
| ], | |
| yaml: [ | |
| ["comment", /#[^\n]*/], | |
| ["string", /'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*"/], | |
| ["keyword", /\b(?:true|false|null|yes|no)\b/], | |
| ["number", /-?\b\d[\d.eE+-]*\b/], | |
| ], | |
| }; | |
| HL_RULES.javascript = HL_RULES.python; | |
| HL_RULES.typescript = HL_RULES.python; | |
| HL_RULES.sql = [ | |
| ["comment", /--[^\n]*/], | |
| ["string", /'(?:\\.|[^'\\])*'/], | |
| [ | |
| "keyword", | |
| /\b(?:SELECT|FROM|WHERE|JOIN|LEFT|RIGHT|INNER|OUTER|ON|GROUP|BY|ORDER|LIMIT|INSERT|INTO|VALUES|UPDATE|SET|DELETE|CREATE|TABLE|AS|AND|OR|NOT|NULL|COUNT|DISTINCT|IN)\b/i, | |
| ], | |
| ["number", /\b\d[\d.]*\b/], | |
| ]; | |
| function highlightCode(code, lang) { | |
| const rules = HL_RULES[lang]; | |
| if (!rules) return esc(code); | |
| const combined = new RegExp(rules.map((r) => "(" + r[1].source + ")").join("|"), "g"); | |
| let out = ""; | |
| let last = 0; | |
| let m; | |
| while ((m = combined.exec(code))) { | |
| if (m[0] === "") { | |
| combined.lastIndex++; | |
| continue; | |
| } | |
| out += esc(code.slice(last, m.index)); | |
| let gi = 1; | |
| while (gi < m.length && m[gi] === undefined) gi++; | |
| out += `<span class="tok-${rules[gi - 1][0]}">${esc(m[0])}</span>`; | |
| last = m.index + m[0].length; | |
| } | |
| out += esc(code.slice(last)); | |
| return out; | |
| } | |
| function renderCode(code, lang, title) { | |
| const pre = document.createElement("pre"); | |
| pre.className = "hl"; | |
| const c = document.createElement("code"); | |
| c.innerHTML = highlightCode(code, lang); | |
| pre.appendChild(c); | |
| if (!title) return pre; | |
| const det = document.createElement("details"); | |
| det.className = "code-accordion"; | |
| const sum = document.createElement("summary"); | |
| sum.innerHTML = `<span class="code-ico"></></span> ${esc(title)}`; | |
| det.appendChild(sum); | |
| det.appendChild(pre); | |
| return det; | |
| } | |
| const IMG_PATH = /^[^\s]+\.(png|jpe?g|gif|svg|webp)$/i; | |
| function renderList(items, container) { | |
| let ul = null; | |
| items.forEach((item) => { | |
| if (URL_ONLY.test(item) || IMG_PATH.test(item)) { | |
| ul = null; | |
| container.appendChild(unfurl(item)); | |
| } else if (item.indexOf("📦 Artifact") !== -1) { | |
| ul = null; | |
| const div = document.createElement("div"); | |
| div.className = "artifact-chip"; | |
| div.innerHTML = inline(item); | |
| container.appendChild(div); | |
| } else if (item.indexOf("trackio-local-dashboard://") !== -1) { | |
| ul = null; | |
| const div = document.createElement("div"); | |
| div.className = "artifact-chip"; | |
| div.innerHTML = | |
| "🎯 <strong>Local dashboard</strong> — publish the logbook to share it"; | |
| container.appendChild(div); | |
| } else { | |
| if (!ul) { | |
| ul = document.createElement("ul"); | |
| container.appendChild(ul); | |
| } | |
| const li = document.createElement("li"); | |
| li.innerHTML = inline(item); | |
| ul.appendChild(li); | |
| } | |
| }); | |
| } | |
| /* -------------------- unfurl providers -------------------- */ | |
| function card(url, kind, icon, title, desc, chips) { | |
| const a = document.createElement("a"); | |
| a.className = "unfurl"; | |
| a.href = url; | |
| a.target = "_blank"; | |
| a.rel = "noopener"; | |
| const chipHtml = (chips || []) | |
| .filter(Boolean) | |
| .map((c) => `<span class="chip">${esc(c)}</span>`) | |
| .join(""); | |
| a.innerHTML = | |
| `<div class="unfurl-body">` + | |
| `<div class="unfurl-ico">${icon}</div>` + | |
| `<div class="unfurl-main">` + | |
| `<div class="unfurl-kind">${esc(kind)}</div>` + | |
| `<div class="unfurl-title">${esc(title)}</div>` + | |
| (desc ? `<div class="unfurl-desc">${esc(desc)}</div>` : "") + | |
| (chipHtml ? `<div class="unfurl-meta">${chipHtml}</div>` : "") + | |
| `</div></div>` + | |
| `<div class="unfurl-raw">${esc(url)}</div>`; | |
| return a; | |
| } | |
| function fmt(n) { | |
| if (n == null) return null; | |
| if (n >= 1e6) return (n / 1e6).toFixed(1) + "M"; | |
| if (n >= 1e3) return (n / 1e3).toFixed(1) + "k"; | |
| return String(n); | |
| } | |
| const providers = [ | |
| { | |
| test: (u) => /\.(png|jpe?g|gif|svg|webp)(\?|$)/i.test(u) || /\/artifact_blob\//.test(u), | |
| render: (u, el) => { | |
| el.className = "unfurl image"; | |
| el.href = u; | |
| const img = document.createElement("img"); | |
| img.loading = "lazy"; | |
| img.src = u; | |
| img.alt = "artifact image"; | |
| el.appendChild(img); | |
| }, | |
| }, | |
| { | |
| test: (u) => /huggingface\.co\/datasets\//.test(u), | |
| render: async (u, el) => { | |
| const id = u.split("/datasets/")[1].split(/[?#]/)[0].replace(/\/$/, ""); | |
| base(el, u, "HF Dataset", "📊", id, "Hugging Face dataset"); | |
| const d = await getJSON(`https://huggingface.co/api/datasets/${id}`); | |
| if (d) | |
| fill(el, id, d.cardData?.pretty_name || id, [ | |
| `↓ ${fmt(d.downloads)}`, | |
| `♥ ${fmt(d.likes)}`, | |
| ...(d.tags || []).filter((t) => !t.includes(":")).slice(0, 3), | |
| ]); | |
| }, | |
| }, | |
| { | |
| test: (u) => /huggingface\.co\/spaces\//.test(u), | |
| embed: true, | |
| render: (u, el) => { | |
| const id = u.split("/spaces/")[1].split(/[?#]/)[0].replace(/\/$/, ""); | |
| const sub = id.toLowerCase().replace(/[^a-z0-9-]/g, "-"); | |
| el.classList.add("embed"); | |
| el.innerHTML = | |
| `<div class="embed-head">` + | |
| `<span class="unfurl-kind">🚀 HF Space</span>` + | |
| `<a class="embed-title" href="${esc(u)}" target="_blank" rel="noopener">${esc(id)}</a>` + | |
| `<a class="embed-open" href="${esc(u)}" target="_blank" rel="noopener">Open ↗</a>` + | |
| `</div>` + | |
| `<iframe class="embed-frame" src="https://${sub}.hf.space/?sidebar=hidden&navbar=hidden" loading="lazy" ` + | |
| `allow="clipboard-read; clipboard-write; fullscreen"></iframe>`; | |
| }, | |
| }, | |
| { | |
| test: (u) => /huggingface\.co\/jobs\//.test(u), | |
| render: (u, el) => { | |
| const rest = u.split("/jobs/")[1].split(/[?#]/)[0].replace(/\/$/, ""); | |
| const parts = rest.split("/"); | |
| const jid = parts[1] || ""; | |
| base( | |
| el, | |
| u, | |
| "HF Job", | |
| "⚙️", | |
| `${parts[0]} · ${jid.slice(0, 12)}${jid.length > 12 ? "…" : ""}`, | |
| "Hugging Face Job — open to view status & logs" | |
| ); | |
| }, | |
| }, | |
| { | |
| test: (u) => /huggingface\.co\/buckets\//.test(u), | |
| render: (u, el) => { | |
| const id = u.split("/buckets/")[1].split(/[?#]/)[0].replace(/\/$/, ""); | |
| base(el, u, "HF Bucket", "🪣", id, "Hugging Face Bucket — stored artifacts & data"); | |
| }, | |
| }, | |
| { | |
| test: (u) => /arxiv\.org\/(abs|pdf)\//.test(u), | |
| render: (u, el) => { | |
| const id = u.split(/\/(abs|pdf)\//)[2].replace(/\.pdf$/, ""); | |
| base(el, u, "arXiv", "📄", `arXiv:${id}`, "Preprint"); | |
| }, | |
| }, | |
| { | |
| test: (u) => /github\.com\/[^/]+\/[^/]+/.test(u), | |
| render: async (u, el) => { | |
| const m = u.match(/github\.com\/([^/]+)\/([^/?#]+)/); | |
| const id = `${m[1]}/${m[2]}`; | |
| base(el, u, "GitHub", "🐙", id, "Repository"); | |
| const d = await getJSON(`https://api.github.com/repos/${id}`); | |
| if (d) | |
| fill(el, id, d.description, [ | |
| `★ ${fmt(d.stargazers_count)}`, | |
| d.language, | |
| ]); | |
| }, | |
| }, | |
| { | |
| test: (u) => /huggingface\.co\/[^/]+\/[^/]+/.test(u), | |
| render: async (u, el) => { | |
| const id = u.split("huggingface.co/")[1].split(/[?#]/)[0].replace(/\/$/, ""); | |
| base(el, u, "HF Model", "🤗", id, "Model on the Hugging Face Hub"); | |
| const d = await getJSON(`https://huggingface.co/api/models/${id}`); | |
| if (d) | |
| fill(el, id, d.pipeline_tag ? `Task: ${d.pipeline_tag}` : null, [ | |
| `↓ ${fmt(d.downloads)}`, | |
| `♥ ${fmt(d.likes)}`, | |
| ...(d.tags || []).filter((t) => !t.includes(":")).slice(0, 2), | |
| ]); | |
| }, | |
| }, | |
| ]; | |
| function base(el, url, kind, icon, title, desc) { | |
| el.className = "unfurl"; | |
| el.href = url; | |
| el.innerHTML = | |
| `<div class="unfurl-body"><div class="unfurl-ico">${icon}</div>` + | |
| `<div class="unfurl-main"><div class="unfurl-kind">${esc(kind)}</div>` + | |
| `<div class="unfurl-title">${esc(title)}</div>` + | |
| `<div class="unfurl-desc">${esc(desc)}</div>` + | |
| `<div class="unfurl-meta"></div></div></div>` + | |
| `<div class="unfurl-raw">${esc(url)}</div>`; | |
| } | |
| function fill(el, title, desc, chips) { | |
| if (title) el.querySelector(".unfurl-title").textContent = title; | |
| const d = el.querySelector(".unfurl-desc"); | |
| if (desc) d.textContent = desc; | |
| const meta = el.querySelector(".unfurl-meta"); | |
| meta.innerHTML = (chips || []) | |
| .filter(Boolean) | |
| .map((c) => `<span class="chip">${esc(c)}</span>`) | |
| .join(""); | |
| } | |
| async function getJSON(url) { | |
| if (UNFURL_CACHE[url] !== undefined) return UNFURL_CACHE[url]; | |
| try { | |
| const r = await fetch(url); | |
| if (!r.ok) throw new Error(r.status); | |
| const j = await r.json(); | |
| UNFURL_CACHE[url] = j; | |
| return j; | |
| } catch (e) { | |
| UNFURL_CACHE[url] = null; | |
| return null; | |
| } | |
| } | |
| function unfurl(url) { | |
| const provider = providers.find((p) => p.test(url)); | |
| const el = document.createElement(provider && provider.embed ? "div" : "a"); | |
| el.className = "unfurl"; | |
| if (el.tagName === "A") { | |
| el.href = url; | |
| el.target = "_blank"; | |
| el.rel = "noopener"; | |
| } | |
| if (provider) { | |
| const out = provider.render(url, el); | |
| if (out && typeof out.then === "function") out.catch(() => {}); | |
| } else { | |
| let host = url; | |
| try { | |
| host = new URL(url).hostname.replace(/^www\./, ""); | |
| } catch (e) {} | |
| base(el, url, "Link", "🔗", host, url); | |
| } | |
| return el; | |
| } | |
| /* -------------------- routing / render -------------------- */ | |
| function buildTree() { | |
| const tree = document.getElementById("tree"); | |
| tree.innerHTML = ""; | |
| const nodes = []; | |
| (MANIFEST.root.children || []).forEach((c) => flattenTree(c, 0, nodes)); | |
| nodes.forEach(({ node, depth }) => { | |
| const a = document.createElement("a"); | |
| a.href = "#/" + node.slug; | |
| a.textContent = node.title; | |
| a.className = "depth-" + depth; | |
| a.dataset.slug = node.slug; | |
| tree.appendChild(a); | |
| }); | |
| } | |
| function highlight(slug) { | |
| document | |
| .querySelectorAll("#tree a") | |
| .forEach((a) => a.classList.toggle("active", a.dataset.slug === slug)); | |
| document | |
| .getElementById("book-head") | |
| .classList.toggle("active", slug === MANIFEST.root.slug); | |
| } | |
| async function loadPage(slug) { | |
| const node = findNode(MANIFEST.root, slug) || MANIFEST.root; | |
| const page = document.getElementById("page"); | |
| page.innerHTML = ""; | |
| if (!PAGE_CACHE[node.file]) { | |
| try { | |
| const r = await fetch("./" + node.file); | |
| PAGE_CACHE[node.file] = await r.text(); | |
| } catch (e) { | |
| PAGE_CACHE[node.file] = "# " + node.title + "\n\n_Could not load page._"; | |
| } | |
| } | |
| renderMarkdown(PAGE_CACHE[node.file], page); | |
| highlight(node.slug); | |
| document.getElementById("content").scrollTo(0, 0); | |
| window.scrollTo(0, 0); | |
| } | |
| function route() { | |
| const slug = (location.hash || "").replace(/^#\//, "") || MANIFEST.root.slug; | |
| loadPage(slug); | |
| } | |
| function setupConnect() { | |
| const space = MANIFEST.space_id; | |
| if (!space) return; | |
| const steps = [ | |
| { t: "Install Trackio, if you don't have it yet.", c: "uv tool install trackio" }, | |
| { t: "Add the Trackio skill for your agent, then reload it.", c: "trackio skills add" }, | |
| { t: "Connect to this logbook.", c: `trackio logbook open ${space}` }, | |
| ]; | |
| const ol = document.getElementById("connect-steps"); | |
| steps.forEach((s, i) => { | |
| const li = document.createElement("li"); | |
| const title = document.createElement("div"); | |
| title.className = "step-title"; | |
| title.textContent = `${i + 1}. ${s.t}`; | |
| const block = document.createElement("div"); | |
| block.className = "codeblock"; | |
| const code = document.createElement("code"); | |
| code.textContent = s.c; | |
| const copy = document.createElement("button"); | |
| copy.className = "copy"; | |
| copy.type = "button"; | |
| copy.title = "Copy"; | |
| copy.textContent = "⧉"; | |
| copy.addEventListener("click", () => copyText(s.c, copy, "⧉")); | |
| block.appendChild(code); | |
| block.appendChild(copy); | |
| li.appendChild(title); | |
| li.appendChild(block); | |
| ol.appendChild(li); | |
| }); | |
| const agentPrompt = | |
| `Read and help maintain this Trackio experiment logbook ("${MANIFEST.title}").\n\n` + | |
| "1. If you don't have Trackio, install it: uv tool install trackio\n" + | |
| "2. Add the Trackio skill for your agent: trackio skills add (then reload)\n" + | |
| `3. Connect to this logbook: trackio logbook open ${space}\n\n` + | |
| "You'll get a compact, token-efficient copy you can read. If I've given you " + | |
| 'write access to the Space, add findings with `trackio logbook note "..." ' + | |
| '--experiment "..."` and they will sync back automatically.'; | |
| const foot = document.getElementById("sidebar-foot"); | |
| foot.hidden = false; | |
| const modal = document.getElementById("modal"); | |
| const open = () => (modal.hidden = false); | |
| const close = () => (modal.hidden = true); | |
| document.getElementById("connect-btn").addEventListener("click", open); | |
| document.getElementById("modal-close").addEventListener("click", close); | |
| modal.querySelector(".modal-backdrop").addEventListener("click", close); | |
| document.addEventListener("keydown", (e) => { | |
| if (e.key === "Escape") close(); | |
| }); | |
| const agentBtn = document.getElementById("copy-agent"); | |
| agentBtn.addEventListener("click", () => | |
| copyText(agentPrompt, agentBtn, "Copy for agent") | |
| ); | |
| } | |
| function copyText(text, btn, restore) { | |
| const done = () => { | |
| const prev = btn.textContent; | |
| btn.textContent = restore === "⧉" ? "✓" : "Copied!"; | |
| btn.classList.add("copied"); | |
| setTimeout(() => { | |
| btn.textContent = restore; | |
| btn.classList.remove("copied"); | |
| }, 1400); | |
| void prev; | |
| }; | |
| if (navigator.clipboard && navigator.clipboard.writeText) { | |
| navigator.clipboard.writeText(text).then(done, done); | |
| } else { | |
| const ta = document.createElement("textarea"); | |
| ta.value = text; | |
| document.body.appendChild(ta); | |
| ta.select(); | |
| try { | |
| document.execCommand("copy"); | |
| } catch (e) {} | |
| document.body.removeChild(ta); | |
| done(); | |
| } | |
| } | |
| async function init() { | |
| MANIFEST = await (await fetch("./logbook.json")).json(); | |
| document.title = MANIFEST.title + " · Trackio Logbook"; | |
| document.getElementById("book-title").textContent = MANIFEST.title; | |
| document.getElementById("book-head").addEventListener("click", () => { | |
| location.hash = "#/" + MANIFEST.root.slug; | |
| }); | |
| buildTree(); | |
| setupConnect(); | |
| window.addEventListener("hashchange", route); | |
| route(); | |
| } | |
| init(); | |
| })(); | |