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) => ({ "&": "&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();
})();