codelion's picture
Visualize: ask a dataset a question, get an exact chart with the rows behind every mark (small datasets first: instant glance charts, batched model labels, drill-down, scoped follow-ups)
917845f verified
Raw History Blame Contribute Delete
25.7 kB
"use strict";
const STEPS = ["Format", "Domains", "Subdomains", "Languages", "Summary", "Review", "Run"];
const LANGS = ["English", "Spanish", "French", "German", "Portuguese", "Italian", "Dutch", "Hindi", "Bengali", "Arabic", "Turkish", "Russian", "Polish", "Ukrainian",
"Chinese", "Japanese", "Korean", "Vietnamese", "Indonesian", "Thai", "Swahili", "Serbian", "Greek", "Hebrew"];
const FORMATS = {
instruction: { name: "Instruction pairs", blurb: "A prompt and the reply you want a model to learn. Use it for supervised fine-tuning.",
eg: [["Prompt", "My hardcover arrived with a bent corner. Can I swap it?"], ["Completion", "Sorry about that. Yes, send a photo of the corner and we will ship a replacement today, no need to return the damaged copy."]] },
preference: { name: "Preference pairs", blurb: "A prompt with a chosen and a rejected reply, ranked by the model. Use it for preference tuning.",
eg: [["Prompt", "Where is order 8824? It was due Monday."], ["Chosen", "It left our warehouse Saturday and is with the courier. Tracking shows delivery Thursday."], ["Rejected", "Orders usually take a while. Please be patient."]] },
};
const S = {
step: 0, maxStep: 0, format: "instruction", domains: [], subs: {}, languages: ["English"], request: "", constraints: "", rows: 50,
blueprint: null, bpBusy: false, bpError: "", model: "", job: null, me: null, pub: { name: "", priv: true, license: "apache-2.0", ok: false, busy: false, err: "", url: "" }, jobRows: [], jobState: null, drawn: 0, printed: 0, pubShown: false, maxRows: 200, domainData: [], runError: "",
};
const $ = (s, el = document) => el.querySelector(s);
const esc = (t) => String(t ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
const sentence = (t) => (t ? t.charAt(0).toUpperCase() + t.slice(1) : t);
async function api(path, body, method) {
const r = await fetch(path, { method: method || (body ? "POST" : "GET"), headers: body ? { "Content-Type": "application/json" } : {}, body: body ? JSON.stringify(body) : undefined });
let data = null; try { data = await r.json(); } catch (e) { /* not json */ }
if (!r.ok) throw new Error((data && (data.detail || data.message)) || `Request failed (${r.status})`);
return data;
}
const spec = () => ({ format: S.format, request: S.request.trim(), domains: S.domains, subdomains: Object.values(S.subs).flat(),
languages: S.languages, constraints: S.constraints, rows: S.rows, blueprint: S.blueprint });
/* ------------------------------------------------------------ chrome */
function renderLedger() {
$("#steps").innerHTML = STEPS.map((n, i) => {
const cls = i === S.step ? "on" : i < S.step ? "done" : "";
return `<li class="${cls}"><button data-go="${i}" ${i > S.maxStep ? "disabled" : ""} ${i === S.step ? 'aria-current="step"' : ""}><span class="n">${i + 1}</span><span>${n}</span></button></li>`;
}).join("");
}
function estimate() {
const per = S.format === "preference" ? 4.4 : 2.5, lat = 5500, workers = 4;
const calls = Math.round(S.rows * per + 2);
const secs = Math.max(10, Math.round((calls * lat) / 1000 / workers));
const t = secs >= 3600 ? `${(secs / 3600).toFixed(1)} hours` : secs >= 90 ? `${Math.round(secs / 60)} minutes` : `${secs} seconds`;
return { calls, time: t };
}
function renderSheet() {
const e = estimate(), subs = Object.values(S.subs).flat();
const line = (k, v) => `<div class="line"><dt>${k}</dt><dd>${v}</dd></div>`;
$("#sheet").innerHTML = `<h2>Your dataset</h2><dl>
${line("Format", esc(FORMATS[S.format].name))}
${line("Domains", S.domains.length ? esc(S.domains.join(", ")) : '<span class="hint">None chosen. The description will guide it.</span>')}
${line("Subdomains", subs.length ? esc(subs.slice(0, 6).join(", ")) + (subs.length > 6 ? ` and ${subs.length - 6} more` : "") : '<span class="hint">All</span>')}
${line("Languages", esc(S.languages.join(", ")))}
${line("Rows", S.rows.toLocaleString())}
${line("Model", esc(S.model || "Free model on Hugging Face"))}
</dl><p class="est">About <b>${e.calls.toLocaleString()}</b> model calls and <b>${e.time}</b> on this setup.</p>`;
}
function go(i) { S.step = i; S.maxStep = Math.max(S.maxStep, i); render(); $("#panel").focus({ preventScroll: true }); window.scrollTo({ top: 0 }); }
function actions(back, nextLabel, nextOk = true, extra = "") {
return `<div class="actions">${back ? `<button class="btn" data-act="back">Back</button>` : ""}${nextLabel ? `<button class="btn go" data-act="next" ${nextOk ? "" : "disabled"}>${nextLabel}</button>` : ""}${extra}</div>`;
}
/* ------------------------------------------------------------ steps */
function stepFormat() {
return `<h1>What kind of rows do you need?</h1>
<p class="lede">Pick the shape of each example. You can describe the content in the steps that follow.</p>
<div class="specimens">${Object.entries(FORMATS).map(([k, f]) => `
<button class="spec" data-format="${k}" aria-pressed="${S.format === k}"><b>${f.name}</b><span class="hint">${f.blurb}</span>
<div class="eg">${f.eg.map(([l, t]) => `<p><u>${l}</u>${esc(t)}</p>`).join("")}</div></button>`).join("")}</div>
${actions(false, "Continue")}`;
}
function stepDomains() {
return `<h2>Which areas should the data cover?</h2>
<p class="lede">Choose any number, or skip this and describe the data yourself in the summary.</p>
<ul class="choices">${S.domainData.map((d) => `<li><button class="choice" data-domain="${esc(d.name)}" aria-pressed="${S.domains.includes(d.name)}"><span class="box"></span><span>${esc(d.name)}</span></button></li>`).join("")}</ul>
${actions(true, "Continue")}`;
}
function stepSubs() {
if (!S.domains.length) return `<h2>Subdomains</h2><p class="lede">No domains chosen, so there is nothing to narrow. Continue to the languages, or go back and pick a domain.</p>${actions(true, "Continue")}`;
return `<h2>Narrow it down</h2><p class="lede">Pick the subdomains you care about. Leave a domain empty to cover all of it.</p>
${S.domains.map((name) => { const d = S.domainData.find((x) => x.name === name); const sel = S.subs[name] || [];
return `<div class="sub-block"><h3>${esc(name)}</h3><ul class="choices">${d.subdomains.map((s) => `<li><button class="choice" data-sub="${esc(name)}|${esc(s)}" aria-pressed="${sel.includes(s)}"><span class="box"></span><span>${esc(sentence(s))}</span></button></li>`).join("")}</ul></div>`; }).join("")}
${actions(true, "Continue")}`;
}
function stepLangs() {
return `<h2>Which languages?</h2><p class="lede">Rows are written directly in each language you choose, split evenly. English is the default.</p>
<ul class="choices">${LANGS.map((l) => `<li><button class="choice" data-lang="${l}" aria-pressed="${S.languages.includes(l)}"><span class="box"></span><span>${l}</span></button></li>`).join("")}</ul>
${actions(true, "Continue", S.languages.length > 0)}`;
}
function bpView() {
const bp = S.blueprint; if (!bp) return "";
return `<div class="bp">
<div class="bp-row"><b>Who answers</b><span>${esc(bp.assistant_role)}</span></div>
<div class="bp-row"><b>Topics</b><div class="tree">${bp.topics.map((a) => `<div><strong>${esc(a.area)}</strong> <small>${a.subtopics.map((s) => esc(s.subtopic)).join(", ")}</small></div>`).join("")}</div></div>
<div class="bp-row"><b>Who asks</b><ul>${bp.personas.map((p) => `<li>${esc(p)}</li>`).join("")}</ul></div>
<div class="bp-row"><b>Question shapes</b><ul>${bp.formats.map((p) => `<li>${esc(p)}</li>`).join("")}</ul></div>
</div>`;
}
function stepSummary() {
const suggest = () => { const d = S.domains.length ? ` covering ${S.domains.join(", ").toLowerCase()}` : ""; return `A dataset of ${S.format === "preference" ? "prompts with a better and a worse reply" : "prompts and ideal replies"}${d}.`; };
return `<h2>Describe the dataset</h2>
<p class="lede">Say what you want in your own words. The more specific you are about who asks and who answers, the better the rows.</p>
<label class="f"><span>What should the dataset contain?</span>
<textarea class="in" id="request" placeholder="Customer support conversations for a small online bookshop: order status, returns, damaged books and gift wrapping.">${esc(S.request)}</textarea></label>
<p style="margin-top:8px"><button class="linkish" data-act="suggest">Draft a description from my choices</button> <span class="hint" data-suggest="${esc(suggest())}"></span></p>
<label class="f"><span>Rules every row must follow <span class="hint">(optional)</span></span>
<textarea class="in" id="constraints" style="min-height:76px" placeholder="Replies must be under 120 words. Use plain text, no markdown.">${esc(S.constraints)}</textarea></label>
<div class="group"><h3>How many rows?</h3>
<div class="seg" role="group" aria-label="Rows">${[25, 50, 100, 200].filter((n) => n <= S.maxRows).map((n) => `<button data-rows="${n}" aria-pressed="${S.rows === n}">${n.toLocaleString()}</button>`).join("")}</div>
<p class="hint" style="margin-top:8px">Hosted runs are capped at ${S.maxRows.toLocaleString()} rows. Run it yourself for more.</p></div>
<div class="group"><h3>Blueprint</h3>
<p class="hint">A plan for coverage: topics, who asks, who answers, and the shapes questions take. Preview it before you spend model calls on rows.</p>
<div class="actions" style="margin-top:12px"><button class="btn" data-act="blueprint" ${S.bpBusy || !S.request.trim() ? "disabled" : ""}>${S.bpBusy ? "Drafting the blueprint" : S.blueprint ? "Draft it again" : "Preview the blueprint"}</button></div>
${S.bpError ? `<p class="note bad">${esc(S.bpError)}</p>` : ""}${bpView()}</div>
${actions(true, "Continue", !!S.request.trim())}`;
}
function stepReview() {
const e = estimate(), subs = Object.values(S.subs).flat();
const row = (k, v) => `<div class="bp-row"><b>${k}</b><span>${v}</span></div>`;
return `<h2>Check it, then start writing</h2>
<p class="lede">Rows are written by a free model on Hugging Face Inference Providers. You can leave this page open or come back to it while the run goes.</p>
<div class="bp">${row("Format", esc(FORMATS[S.format].name))}${row("Description", esc(S.request))}
${row("Domains", S.domains.length ? esc(S.domains.join(", ")) : "Guided by the description")}${row("Subdomains", subs.length ? esc(subs.join(", ")) : "All")}
${row("Languages", esc(S.languages.join(", ")))}${S.constraints.trim() ? row("Rules", esc(S.constraints)) : ""}${row("Rows", S.rows.toLocaleString())}
${row("Blueprint", S.blueprint ? "Previewed. It will be used as shown." : "Drafted when the run starts.")}</div>
<p class="note">About <b>${e.calls.toLocaleString()}</b> model calls and <b>${e.time}</b>. Rows that fail the quality checks are rewritten, so a run can take a little longer.</p>
<div class="actions"><button class="btn" data-act="back">Back</button><button class="btn go" data-act="next">Start writing</button></div>`;
}
function publishHtml() {
const j = S.jobState; if (!isFinal(j) || !j.count) return "";
const p = S.pub, me = S.me || {};
if (p.url) return `<div class="group"><h3>Published</h3><p class="note ok">Your dataset is on Hugging Face${p.priv ? " as a private dataset" : ""}. <a href="${esc(p.url)}" target="_blank" rel="noopener">Open it</a>.</p></div>`;
const head = `<h3>Publish to your Hugging Face account</h3>`;
if (!me.user) {
if (!me.can_sign_in) return `<div class="group">${head}<p class="hint">Publishing works on the hosted app. Download the CSV or JSONL instead.</p></div>`;
const embedded = window.top !== window.self, href = embedded && me.host ? `https://${me.host}/oauth/login?job=${j.id}` : `/oauth/login?job=${j.id}`;
return `<div class="group">${head}<p class="hint" style="max-width:60ch">Sign in to save this dataset to your own account, with a card that says how it was made. DataInventor can only create datasets for you. It cannot see your other repos. It is private unless you choose public.</p>
<div class="actions" style="margin-top:12px"><a class="btn go" href="${esc(href)}" ${embedded ? 'target="_blank" rel="noopener"' : ""} style="text-decoration:none">Sign in with Hugging Face</a></div>${embedded ? '<p class="hint" style="margin-top:8px">Sign-in opens in its own tab because browsers block it inside an embedded page.</p>' : ""}</div>`;
}
if (!p.name) p.name = "datainventor-" + (j.slug || "dataset");
return `<div class="group">${head}<p class="hint">Signed in as <b>${esc(me.user)}</b>. <button class="linkish" data-act="signout">Sign out</button></p>
<div class="row2"><label class="f"><span>Dataset name</span><input class="in" id="pub-name" value="${esc(p.name)}" spellcheck="false"></label>
<label class="f"><span>Who can see it</span><select class="in" id="pub-private"><option value="1" ${p.priv ? "selected" : ""}>Only me (private)</option><option value="0" ${p.priv ? "" : "selected"}>Everyone (public)</option></select></label></div>
<label class="f" style="max-width:320px"><span>License</span><select class="in" id="pub-license">${["apache-2.0", "cc-by-4.0", "cc-by-sa-4.0", "mit", "cc0-1.0"].map((l) => `<option ${p.license === l ? "selected" : ""}>${l}</option>`).join("")}</select></label>
<label class="check"><input type="checkbox" id="pub-ok" ${p.ok ? "checked" : ""}><span>I have the right to publish this data. I understand it was written by a model and has not been checked by a person.</span></label>
${p.err ? `<p class="note bad">${esc(p.err)}</p>` : ""}
<div class="actions"><button class="btn go" data-act="publish" ${p.busy ? "disabled" : ""}>${p.busy ? "Publishing" : "Publish dataset"}</button></div></div>`;
}
async function loadMe() { try { S.me = await api("/api/me"); } catch (e) { S.me = { user: null, can_sign_in: false }; } const el = document.getElementById("run-publish"); if (el) el.innerHTML = publishHtml(); }
async function doPublish() {
S.pub.busy = true; S.pub.err = ""; refreshPublish();
try { const r = await api(`/api/jobs/${S.job}/publish`, { name: S.pub.name.trim(), private: S.pub.priv, license: S.pub.license, confirm: S.pub.ok }); S.pub.url = r.url; } catch (e) { S.pub.err = e.message; }
S.pub.busy = false; refreshPublish();
}
function refreshPublish() { const el = document.getElementById("run-publish"); if (el) el.innerHTML = publishHtml(); }
async function restoreJob(id) {
try {
let since = 0, last = null; S.jobRows = [];
for (let k = 0; k < 8; k++) { last = await api(`/api/jobs/${id}?since=${since}`); S.jobRows.push(...last.new_rows); since = S.jobRows.length; if (since >= last.count) break; }
const c = last.config || {}; S.job = id; S.jobState = last; S.format = c.format || "instruction"; S.request = c.request || ""; S.domains = c.domains || []; S.languages = c.languages || ["English"]; S.constraints = c.constraints || ""; S.rows = c.rows || S.rows;
S.maxStep = 6; S.step = 6; S.printed = S.jobRows.length ? S.jobRows[S.jobRows.length - 1].id : 0; render();
if (!isFinal(last)) poll();
} catch (e) { /* the job is gone; start fresh */ }
}
const isFinal = (j) => j && ["done", "stopped", "failed"].includes(j.state);
function rowHtml(r) {
const pref = S.format === "preference";
return `<div class="trow"><span class="id">${r.id}</span><div><div class="p">${esc(r.prompt)}</div><div class="meta">${esc(r.area)}, ${esc(r.persona)}${r.language && r.language !== "English" ? ", " + esc(r.language) : ""}</div>
<details><summary>${pref ? "Read both replies" : "Read the reply"}</summary><div class="full">${pref ? "Chosen\n" + esc(r.chosen) + "\n\nRejected\n" + esc(r.rejected) : esc(r.completion)}</div></details></div></div>`;
}
function deckHtml() {
const pref = S.format === "preference", latest = S.jobRows.slice(-3);
const fresh = latest.length ? latest[latest.length - 1].id : 0;
const out = latest.map((r, i) => `<div class="card ${i === latest.length - 1 && fresh > S.printed ? "print" : ""}" style="transform:translate(${i * 16}px, ${i * 14}px);z-index:${i}"><em>${esc(r.persona)}, ${esc(r.topic)}</em><div class="clip">${esc(r.prompt)}</div><div class="a clip">${esc(pref ? r.chosen : r.completion)}</div></div>`).join("");
S.printed = Math.max(S.printed, fresh);
return out;
}
function headParts() {
const j = S.jobState, n = j ? j.count : 0, t = j ? j.target || S.rows : S.rows, pct = t ? Math.min(100, Math.round((n / t) * 100)) : 0, fin = isFinal(j);
const st = j && j.stats && j.stats.rows ? j.stats : null;
return {
h2: fin ? (j.state === "failed" ? "The run stopped" : "Your dataset is ready") : "Writing your dataset",
count: `<span class="big">${n.toLocaleString()}</span><span class="of">of ${t.toLocaleString()} rows</span>`, pct,
state: `${j ? esc(j.message) : "Starting"}${j && !fin ? ` Elapsed ${Math.round(j.elapsed)} s.` : ""}`,
note: j && j.state === "failed" ? `<p class="note bad">${esc(j.message)}</p>` : j && j.state === "done" && j.count < j.target ? `<p class="note">${esc(j.message)}</p>` : "",
actions: fin ? `${j.count ? `<a class="btn go" href="/api/jobs/${j.id}/download?format=csv" style="text-decoration:none">Download CSV</a><a class="btn" href="/api/jobs/${j.id}/download?format=jsonl" style="text-decoration:none">Download JSONL</a>` : ""}${j.count ? `<button class="btn" data-act="visualize">Visualize these rows</button>` : ""}<button class="btn" data-act="again">Start another</button>` : `<button class="btn" data-act="stop">Stop and keep what is written</button>`,
stats: st ? `<div class="stats"><div><b>${st.rows.toLocaleString()}</b><span>rows</span></div><div><b>${st.distinct3_prompts}</b><span>distinct 3-grams in prompts</span></div><div><b>${st.avg_prompt_words}</b><span>words per prompt</span></div><div><b>${st.avg_answer_words}</b><span>words per reply</span></div></div>` : "",
filtered: j && j.rejected && Object.keys(j.rejected).length ? `<p class="hint" style="margin-top:12px">Filtered out: ${Object.entries(j.rejected).map(([k, v]) => `${v} ${esc(k)}`).join(", ")}.</p>` : "",
};
}
function stepRun() {
if (S.runError && !S.jobState) return `<h2>The run did not start</h2><p class="note bad">${esc(S.runError)}</p>${actions(true, "Try again")}`;
const h = headParts(); S.drawn = S.jobRows.length;
return `<div id="runroot"><h2 id="run-h2">${h.h2}</h2>
<div class="count" id="run-count" aria-live="polite">${h.count}</div>
<div class="bar" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${h.pct}"><i id="run-bar" style="width:${h.pct}%"></i></div>
<p class="state" id="run-state">${h.state}</p><div id="run-note">${h.note}</div>
<div class="actions" id="run-actions">${h.actions}</div><div id="run-stats">${h.stats}</div><div id="run-filtered">${h.filtered}</div><div id="run-publish">${publishHtml()}</div>
<div class="press" id="run-deck" aria-hidden="true">${deckHtml()}</div>
<div class="table" id="run-rows">${S.jobRows.slice().reverse().map(rowHtml).join("")}</div></div>`;
}
/* patch the run view in place so open replies stay open and only a new card prints */
function updateRun() {
const h = headParts(), set = (id, html) => { const el = document.getElementById(id); if (el && el.innerHTML !== html) el.innerHTML = html; };
set("run-h2", h.h2); set("run-count", h.count); set("run-state", h.state); set("run-note", h.note); set("run-actions", h.actions); set("run-stats", h.stats); set("run-filtered", h.filtered);
if (isFinal(S.jobState) && !S.pubShown) { S.pubShown = true; loadMe(); }
const bar = document.getElementById("run-bar"); if (bar) { bar.style.width = h.pct + "%"; bar.parentElement.setAttribute("aria-valuenow", h.pct); }
const deck = document.getElementById("run-deck"); if (deck) deck.innerHTML = deckHtml();
const rows = document.getElementById("run-rows");
if (rows && S.jobRows.length > S.drawn) { rows.insertAdjacentHTML("afterbegin", S.jobRows.slice(S.drawn).reverse().map(rowHtml).join("")); S.drawn = S.jobRows.length; }
}
/* ------------------------------------------------------------ render + events */
function render() {
if (window.DI_MODE !== "invent") return;
renderLedger(); renderSheet();
const fn = [stepFormat, stepDomains, stepSubs, stepLangs, stepSummary, stepReview, stepRun][S.step];
const keep = document.activeElement && document.activeElement.id;
$("#panel").innerHTML = fn();
if (keep) { const el = document.getElementById(keep); if (el) { el.focus(); if (el.setSelectionRange && el.type !== "number") { const l = el.value.length; try { el.setSelectionRange(l, l); } catch (e) { /* ok */ } } } }
}
document.addEventListener("click", async (ev) => {
const t = ev.target.closest("button, a"); if (!t) return;
if (t.dataset.go !== undefined) return go(+t.dataset.go);
if (t.dataset.format) { S.format = t.dataset.format; S.blueprint = null; return render(); }
if (t.dataset.domain) { const d = t.dataset.domain; S.domains = S.domains.includes(d) ? S.domains.filter((x) => x !== d) : [...S.domains, d]; if (!S.domains.includes(d)) delete S.subs[d]; S.blueprint = null; return render(); }
if (t.dataset.sub) { const [d, s] = t.dataset.sub.split("|"); const cur = S.subs[d] || []; S.subs[d] = cur.includes(s) ? cur.filter((x) => x !== s) : [...cur, s]; S.blueprint = null; return render(); }
if (t.dataset.lang) { const l = t.dataset.lang; S.languages = S.languages.includes(l) ? S.languages.filter((x) => x !== l) : [...S.languages, l]; S.blueprint = null; return render(); }
if (t.dataset.rows) { S.rows = +t.dataset.rows; return render(); }
const act = t.dataset.act; if (!act) return;
if (act === "next") return void go(S.step + 1);
if (act === "back") return go(Math.max(0, S.step - 1));
if (act === "suggest") { S.request = S.request.trim() ? S.request : $("[data-suggest]").dataset.suggest; return render(); }
if (act === "blueprint") return doBlueprint();
if (act === "publish") return doPublish();
if (act === "signout") { await api("/api/signout", {}); S.me = { ...S.me, user: null }; return refreshPublish(); }
if (act === "visualize") { if (S.job && window.DI_visualizeJob) window.DI_visualizeJob(S.job); return; }
if (act === "stop") { if (S.job) await api(`/api/jobs/${S.job}/stop`, {}); return; }
if (act === "again") { S.job = null; S.jobState = null; S.jobRows = []; S.drawn = 0; S.printed = 0; S.pubShown = false; S.pub = { name: "", priv: true, license: "apache-2.0", ok: false, busy: false, err: "", url: "" }; S.runError = ""; return go(4); }
});
document.addEventListener("change", (ev) => { if (ev.target.id && ev.target.id.startsWith("pub-")) { const id = ev.target.id; if (id === "pub-private") S.pub.priv = ev.target.value === "1"; else if (id === "pub-license") S.pub.license = ev.target.value; else if (id === "pub-ok") S.pub.ok = ev.target.checked; } });
document.addEventListener("input", (ev) => {
const id = ev.target.id;
if (id === "pub-name") S.pub.name = ev.target.value; else if (id === "pub-private") S.pub.priv = ev.target.value === "1"; else if (id === "pub-license") S.pub.license = ev.target.value; else if (id === "pub-ok") S.pub.ok = ev.target.checked;
if (id === "request") { S.request = ev.target.value; S.blueprint = null; const b = $('[data-act="next"]'); if (b) b.disabled = !S.request.trim(); const bp = $('[data-act="blueprint"]'); if (bp) bp.disabled = S.bpBusy || !S.request.trim(); }
else if (id === "constraints") { S.constraints = ev.target.value; S.blueprint = null; }
});
async function doBlueprint() {
S.bpBusy = true; S.bpError = ""; render();
try { S.blueprint = await api("/api/blueprint", spec()); } catch (e) { S.bpError = e.message; }
S.bpBusy = false; render();
}
async function startRun() {
S.runError = ""; S.jobRows = []; S.jobState = null; S.drawn = 0; S.printed = 0; S.pubShown = false; S.pub = { name: "", priv: true, license: "apache-2.0", ok: false, busy: false, err: "", url: "" };
try { const r = await api("/api/jobs", spec()); S.job = r.id; poll(); } catch (e) { S.runError = e.message; }
render();
}
async function poll() {
const id = S.job; if (!id) return;
try {
const j = await api(`/api/jobs/${id}?since=${S.jobRows.length}`);
if (id !== S.job) return;
const wasFinal = isFinal(S.jobState); S.jobRows.push(...j.new_rows); S.jobState = j;
if (S.step === 6) { if (document.getElementById("runroot") && !wasFinal) updateRun(); else render(); }
if (!["done", "stopped", "failed"].includes(j.state) || j.count > S.jobRows.length) return void setTimeout(poll, j.count > S.jobRows.length ? 200 : 1500);
if (j.stats && j.stats.rows) S.maxStep = 6;
} catch (e) { console.error(e.stack); S.jobState = { state: "failed", message: e.message, count: S.jobRows.length, target: S.rows, elapsed: 0, id }; if (S.step === 6) render(); }
}
window.DI_renderInvent = () => render();
window.DI_lastJob = () => (S.job && isFinal(S.jobState) && S.jobState.count ? { id: S.job, n: S.jobState.count, request: S.request || "" } : null);
/* "Start writing" on the review step starts the run */
const _go = go;
go = function (i) { const wasReview = S.step === 5; _go(i); if (i === 6 && wasReview && !S.jobState) startRun(); return true; };
(async function init() {
try { const d = await api("/api/defaults"); S.model = d.model; S.maxRows = d.max_rows; S.configured = d.configured; if (S.rows > S.maxRows) S.rows = S.maxRows; } catch (e) { /* offline */ }
try { S.domainData = await api("/api/domains"); } catch (e) { S.domainData = []; }
const q = new URLSearchParams(location.search), jid = q.get("job");
if (jid) { await loadMe(); await restoreJob(jid); history.replaceState(null, "", "/"); } else render();
})();