Spaces:
Running
Running
File size: 25,669 Bytes
09e451a 33e08f1 09e451a b605207 4d9dbcd 09e451a 33e08f1 09e451a 33e08f1 09e451a 33e08f1 09e451a 33e08f1 09e451a b605207 09e451a 33e08f1 09e451a 4d9dbcd 09e451a 917845f 09e451a 4d9dbcd 09e451a 4d9dbcd 09e451a 917845f 09e451a 33e08f1 09e451a 4d9dbcd 917845f 09e451a 4d9dbcd 09e451a 4d9dbcd 09e451a 4d9dbcd 09e451a 4d9dbcd 09e451a 4f740eb 917845f 33e08f1 09e451a 33e08f1 09e451a 33e08f1 09e451a 4d9dbcd 09e451a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 | "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) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[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();
})();
|