File size: 36,392 Bytes
4f740eb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
917845f
 
 
 
 
4f740eb
 
 
917845f
4f740eb
917845f
4f740eb
917845f
 
4f740eb
917845f
4f740eb
917845f
 
4f740eb
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
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
"use strict";
/* Curate a corpus: register sources and targets, then clean, calibrate, create, compose.
   The hosted run works on a sample; the recipe runs in full on your own machines. */
(function () {
  const CSTEPS = ["Sources", "Clean", "Calibrate", "Create", "Compose", "Run"];
  const BUDGETS = [["1B", 1e9], ["10B", 1e10], ["100B", 1e11], ["1T", 1e12]];
  const BENCH_NAMES = { gsm8k: "GSM8K", math500: "MATH-500", humaneval: "HumanEval", mbpp: "MBPP", mmlu: "MMLU", arc_challenge: "ARC-Challenge",
    arc_easy: "ARC-Easy", hellaswag: "HellaSwag", openbookqa: "OpenBookQA", winogrande: "WinoGrande", truthfulqa: "TruthfulQA", commonsenseqa: "CommonsenseQA" };
  const KIND_NAMES = { web: "Web", educational: "Educational web", textbook: "Textbook-style", math: "Math", code: "Code", multilingual: "Multilingual", synthetic: "Synthetic", other: "Other" };
  const RULES = [["min_words", "Fewest words in a document", 1], ["max_dup_lines", "Most repeated lines (share)", 0.01], ["max_dup10gram", "Most repeated passages (share)", 0.01], ["min_alpha_words", "Fewest words with letters (share)", 0.01]];
  const PRESET = [
    { dataset: "codelion/finepdfs-1B", kind: "textbook" }, { dataset: "codelion/dclm-baseline-1B", kind: "web" }, { dataset: "codelion/fineweb-edu-1B", kind: "educational" }];

  const C = {
    step: 0, maxStep: 0, d: null, sources: [], targets: [], targetsDesc: "", add: "", addRole: "source", addBusy: false, addErr: "",
    decon: ["gsm8k", "math500", "humaneval", "mmlu", "arc_challenge", "hellaswag"], rules: {},
    focus: 0.3, keepTop: 0.6, near: true, threshold: 0.8, gradeDocs: 2000, scorer: "learned",
    rephraseOn: true, styles: [], maxNew: 0.35, inventOn: false, inventReq: "", inventDocs: 0,
    mixes: [{ name: "main", budget: 1e10, weights: {} }], maxEpochs: 4, maxSynth: 0.25,
    tryText: "", tryBusy: false, tryErr: "", tryRes: null, tryFrom: "",
    pid: null, pv: null, runErr: "",
  };
  const $ = (s, el = document) => el.querySelector(s);
  const esc = (t) => String(t ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
  const fmt = (n) => { n = +n || 0; for (const [u, d] of [["T", 1e12], ["B", 1e9], ["M", 1e6], ["K", 1e3]]) if (Math.abs(n) >= d) return `${+(n / d).toPrecision(3)}${u}`; return `${Math.round(n)}`; };
  const pct = (x) => `${Math.round((+x || 0) * 100)}%`;
  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;
  }

  /* ------------------------------------------------------------ recipe */
  function srcOut(s) { return { name: s.name, dataset: s.dataset, config: s.config, split: s.split, text_field: s.text_field, kind: s.kind, num_rows: s.num_rows || 0 }; }
  function recipe() {
    return {
      name: "my-corpus", sources: C.sources.map(srcOut),
      targets: { description: C.targetsDesc.trim(), datasets: C.targets.map(srcOut) },
      clean: { decontaminate: C.decon, rules: C.rules },
      calibrate: { dedup: { near: C.near, threshold: C.threshold }, grade_docs: C.gradeDocs, scorer: C.scorer, focus: C.focus, keep_top: C.keepTop },
      create: { rephrase: { enabled: C.rephraseOn, styles: C.styles, max_new_share: C.maxNew },
        invent: { enabled: C.inventOn && !!C.inventReq.trim(), request: C.inventReq.trim(), docs: C.inventDocs } },
      compose: { mixes: C.mixes.map((m) => ({ name: m.name, budget_tokens: m.budget, weights: m.weights })), max_epochs: C.maxEpochs, max_synthetic_share: C.maxSynth },
    };
  }
  /* mirrors datainventor.compose.default_weights */
  function defaultWeights() {
    const G = { textbook: "textbook", synthetic: "textbook", web: "web", multilingual: "web", other: "web", educational: "educational", math: "math", code: "code" };
    const BASE = { textbook: 0.5, web: 0.3, educational: 0.2 };
    const groups = {}; C.sources.forEach((s) => (groups[G[s.kind] || "web"] = groups[G[s.kind] || "web"] || []).push(s.name));
    const fixed = {}; ["math", "code"].forEach((g) => { if (groups[g]) fixed[g] = 0.1; });
    const base = Object.fromEntries(Object.entries(BASE).filter(([g]) => groups[g]));
    const out = {};
    if (!Object.keys(base).length) { C.sources.forEach((s) => (out[s.name] = 1 / C.sources.length)); return out; }
    const rest = 1 - Object.values(fixed).reduce((a, b) => a + b, 0), tot = Object.values(base).reduce((a, b) => a + b, 0);
    const shares = { ...Object.fromEntries(Object.entries(base).map(([g, v]) => [g, (rest * v) / tot])), ...fixed };
    Object.entries(groups).forEach(([g, names]) => names.forEach((n) => (out[n] = shares[g] / names.length)));
    return out;
  }
  const weightsOf = (m) => ({ ...defaultWeights(), ...m.weights });

  /* ------------------------------------------------------------ chrome */
  function ledger() {
    $("#steps").innerHTML = CSTEPS.map((n, i) => {
      const cls = i === C.step ? "on" : i < C.step ? "done" : "";
      return `<li class="${cls}"><button data-cgo="${i}" ${i > C.maxStep ? "disabled" : ""} ${i === C.step ? 'aria-current="step"' : ""}><span class="n">${i + 1}</span><span>${n}</span></button></li>`;
    }).join("");
  }
  function sheet() {
    const line = (k, v) => `<div class="line"><dt>${k}</dt><dd>${v}</dd></div>`;
    const m = C.mixes[0];
    $("#sheet").innerHTML = `<h2>Your recipe</h2><dl>
      ${line("Sources", C.sources.length ? esc(C.sources.map((s) => s.name).join(", ")) : '<span class="hint">None yet</span>')}
      ${line("Targets", C.targetsDesc.trim() || C.targets.length ? esc([C.targetsDesc.trim().slice(0, 80), ...C.targets.map((t) => t.name)].filter(Boolean).join("; ")) : '<span class="hint">General quality only</span>')}
      ${line("Decontam", C.decon.length ? esc(C.decon.map((b) => BENCH_NAMES[b] || b).join(", ")) : "Off")}
      ${line("Focus", C.focus === 0 || !(C.targetsDesc.trim() || C.targets.length) ? "General quality" : `${pct(1 - C.focus)} quality, ${pct(C.focus)} targets`)}
      ${line("Keep", `Top ${pct(C.keepTop)} by score`)}
      ${line("Create", C.rephraseOn ? "Rephrase to fill gaps" + (C.inventOn ? ", invent" : "") : C.inventOn ? "Invent" : "Off")}
      ${line("Mixes", C.mixes.map((x) => `${esc(x.name)} ${fmt(x.budget)}`).join(", "))}
    </dl>
    <p class="est">The hosted run works on about <b>${C.d ? C.d.per_source : 200}</b> documents per source. The recipe runs the same steps on every document, on your machines.</p>
    <div class="actions" style="margin-top:14px"><button class="btn" data-c="recipe" ${C.sources.length ? "" : "disabled"}>Download recipe</button></div>`;
  }
  function actions(back, next, ok = true, extra = "") {
    return `<div class="actions">${back ? `<button class="btn" data-c="back">Back</button>` : ""}${next ? `<button class="btn go" data-c="next" ${ok ? "" : "disabled"}>${next}</button>` : ""}${extra}</div>`;
  }
  function check(on, attr, label, hint = "") {
    return `<button class="choice" ${attr} aria-pressed="${on}"><span class="box"></span><span>${label}${hint ? ` <span class="hint">${hint}</span>` : ""}</span></button>`;
  }
  function slider(id, label, val, min, max, step, show) {
    return `<label class="f"><span>${label} <b class="sv" id="${id}-v">${show(val)}</b></span><input type="range" class="range" id="${id}" min="${min}" max="${max}" step="${step}" value="${val}"></label>`;
  }

  /* ------------------------------------------------------------ steps */
  function srcRow(s, i, role) {
    const kinds = C.d ? C.d.kinds : Object.keys(KIND_NAMES);
    return `<div class="srow">
      <div class="sname"><b>${esc(s.dataset)}</b><span class="hint">${esc(s.config)} / ${esc(s.split)} / column <code>${esc(s.text_field)}</code>${s.num_rows ? ` / ${fmt(s.num_rows)} rows` : ""}</span>
        ${s.preview ? `<details><summary>Peek at a document</summary><div class="peek">${esc(s.preview)}</div></details>` : ""}</div>
      <div class="sctl">${role === "source" ? `<select class="in sm" id="c-kind-${i}" aria-label="Kind of data">${kinds.map((k) => `<option value="${k}" ${s.kind === k ? "selected" : ""}>${KIND_NAMES[k] || k}</option>`).join("")}</select>` : ""}
        ${s.columns && s.columns.length > 1 ? `<select class="in sm" id="c-col-${role}-${i}" aria-label="Text column">${s.columns.map((c) => `<option ${c === s.text_field ? "selected" : ""}>${esc(c)}</option>`).join("")}</select>` : ""}
        <button class="linkish" data-c="rm" data-role="${role}" data-i="${i}">Remove</button></div></div>`;
  }
  function stepSources() {
    return `<h1>Bring the data you have</h1>
    <p class="lede">Register each Hugging Face dataset as a source to curate, or as a target that shows what good looks like. Targets steer every later step toward your tasks.</p>
    <div class="group"><h3>Add a dataset</h3>
      <div class="addrow"><input class="in" id="c-add" placeholder="HuggingFaceFW/fineweb-edu" value="${esc(C.add)}" spellcheck="false" autocomplete="off">
        <select class="in sm" id="c-addrole" aria-label="Register as"><option value="source" ${C.addRole === "source" ? "selected" : ""}>Source</option><option value="target" ${C.addRole === "target" ? "selected" : ""}>Target</option></select>
        <button class="btn" data-c="add" ${C.addBusy || !C.add.trim() ? "disabled" : ""}>${C.addBusy ? "Reading" : "Add"}</button></div>
      ${C.addErr ? `<p class="note bad">${esc(C.addErr)}</p>` : ""}
      ${C.sources.length ? "" : `<p class="hint" style="margin-top:10px">Or start from <button class="linkish" data-c="preset">the 1B-token mix</button>: textbook-style PDFs, DCLM and FineWeb-Edu.</p>`}</div>
    <div class="group"><h3>Sources <span class="hint">(up to ${C.d ? C.d.max_sources : 4} here)</span></h3>
      ${C.sources.length ? C.sources.map((s, i) => srcRow(s, i, "source")).join("") : '<p class="hint">None yet.</p>'}</div>
    <div class="group"><h3>Targets</h3>
      <label class="f" style="margin-top:6px"><span>What should the trained model be good at? <span class="hint">(optional)</span></span>
        <textarea class="in" id="c-tdesc" style="min-height:80px" placeholder="Reading and drafting commercial contracts. Grade-school math word problems.">${esc(C.targetsDesc)}</textarea></label>
      ${C.targets.map((s, i) => srcRow(s, i, "target")).join("")}
      <p class="hint" style="margin-top:8px">Target datasets are examples of the work you care about, such as a benchmark's training split or your own documents.</p></div>
    ${actions(false, "Continue", C.sources.length > 0)}`;
  }
  function stepClean() {
    const b = C.d ? C.d.benchmarks : Object.keys(BENCH_NAMES), r = { ...(C.d ? C.d.clean_rules : {}), ...C.rules };
    return `<h2>Clean</h2>
    <p class="lede">Heuristic filters drop broken text, boilerplate, empty and repetitive documents. Math and code get their own looser rules. Then any document that shares a 13-word run with a test set you name is removed.</p>
    <div class="group"><h3>Decontaminate against</h3><ul class="choices">${b.map((k) => `<li>${check(C.decon.includes(k), `data-bench="${k}"`, BENCH_NAMES[k] || k)}</li>`).join("")}</ul>
      <p class="hint" style="margin-top:6px">To check against test sets of your own, list them under <code>clean.held_out</code> in the recipe, the same way as sources.</p></div>
    <div class="group"><h3>Filters</h3><div class="row2">${RULES.map(([k, label, step]) => `<label class="f"><span>${label}</span><input class="in" type="number" id="c-rule-${k}" step="${step}" value="${r[k]}"></label>`).join("")}</div>
      <p class="hint" style="margin-top:8px">The defaults follow the Gopher and C4 rules with FineWeb's thresholds. Every rule is in the recipe.</p></div>
    ${actions(true, "Continue")}`;
  }
  function stepCalibrate() {
    const hasT = !!(C.targetsDesc.trim() || C.targets.length);
    return `<h2>Calibrate</h2>
    <p class="lede">A model grades a sample of each source for teaching value, clarity, accuracy and density${hasT ? ", and for relevance to your targets" : ""}. A small scorer learns those grades and scores every document. Redundant copies are dropped first.</p>
    ${hasT ? slider("c-focus", "Balance between general quality and your targets", C.focus, 0, 1, 0.05, (v) => (+v === 0 ? "general quality only" : `${pct(1 - v)} quality / ${pct(v)} targets`))
      : `<p class="note">No targets, so documents are ranked by general quality. Add targets in step 1 to steer the ranking.</p>`}
    ${hasT ? '<p class="hint">Keep some weight on general quality. Data tuned only to the targets overfits them.</p>' : ""}
    ${slider("c-keep", "Keep the best", C.keepTop, 0.1, 1, 0.05, (v) => `${pct(v)} of each source`)}
    <div class="group"><h3>Duplicates</h3><ul class="choices" style="columns:1">
      <li>${check(true, "disabled", "Exact copies", "always removed")}</li>
      <li>${check(C.near, 'data-c="near"', "Near copies", "MinHash, about 80% overlap")}</li></ul></div>
    <div class="group"><h3>On your machines</h3>
      <div class="row2"><label class="f"><span>Documents to grade with the model</span><input class="in" type="number" id="c-grade" min="100" step="100" value="${C.gradeDocs}"></label>
      <label class="f"><span>Score every document by</span><select class="in" id="c-scorer"><option value="learned" ${C.scorer === "learned" ? "selected" : ""}>A scorer learned from the grades</option><option value="llm" ${C.scorer === "llm" ? "selected" : ""}>The model itself (slow, best)</option></select></label></div>
      <p class="hint" style="margin-top:8px">The hosted run grades ${C.d ? 48 : "a few dozen"} documents, enough to see the rubric work but not to trust the learned scorer. In our test, 300 grades gave a rank agreement of about 0.5 with the model; more grades help.</p></div>
    ${actions(true, "Continue")}`;
  }
  function tryHtml() {
    if (C.tryErr) return `<p class="note bad">${esc(C.tryErr)}</p>`;
    if (!C.tryRes) return "";
    return `<div class="pairs">${C.tryRes.results.map((o) => `<div class="pcard"><div class="ph"><b>${esc(o.name)}</b>${o.ok ? "" : `<span class="flag">${esc(o.why)}</span>`}</div>
      <div class="ptext">${esc(o.text || "No rewrite. The model call failed.")}</div>
      ${o.fidelity && (o.fidelity.new_numbers.length || o.fidelity.new_names.length) ? `<p class="hint">Not in the source: ${esc([...o.fidelity.new_numbers, ...o.fidelity.new_names].join(", "))}</p>` : ""}</div>`).join("")}</div>`;
  }
  function stepCreate() {
    const st = C.d ? C.d.styles : {}, picked = C.styles;
    const first = C.sources.find((s) => s.preview);
    return `<h2>Create</h2>
    <p class="lede">Where a source is too small for its share of a mix, the best documents are rephrased into new ones: a dialogue, a tutorial, questions and answers, dense notes, a textbook section. Rewrites that add numbers or names the source does not contain are flagged and dropped.</p>
    <ul class="choices" style="columns:1;margin-top:20px"><li>${check(C.rephraseOn, 'data-c="reph"', "Rephrase to fill gaps")}</li></ul>
    ${C.rephraseOn ? `<div class="group"><h3>Styles <span class="hint">(none picked = chosen per kind of data)</span></h3><ul class="choices">${Object.entries(st).map(([k, n]) => `<li>${check(picked.includes(k), `data-style="${k}"`, esc(n))}</li>`).join("")}</ul></div>
    <div class="group"><h3>Try it on one document</h3>
      <textarea class="in" id="c-try" style="min-height:120px" placeholder="Paste a passage of 60 words or more.">${esc(C.tryText)}</textarea>
      <div class="actions" style="margin-top:12px">${first ? `<button class="btn" data-c="tryfill">Use a document from ${esc(first.name)}</button>` : ""}
        <button class="btn go" data-c="try" ${C.tryBusy || C.tryText.trim().split(/\s+/).length < 60 ? "disabled" : ""}>${C.tryBusy ? "Rewriting" : "Rewrite it in four styles"}</button>${C.tryBusy ? '<span class="hint">This takes about a minute.</span>' : ""}</div>
      <div id="c-tryout">${tryHtml()}</div></div>` : ""}
    <ul class="choices" style="columns:1;margin-top:26px"><li>${check(C.inventOn, 'data-c="inv"', "Also write new documents on a subject", "for areas your sources barely cover")}</li></ul>
    ${C.inventOn ? `<div class="row2"><label class="f"><span>Subject</span><input class="in" id="c-invreq" value="${esc(C.inventReq)}" placeholder="Contract law for small businesses"></label>
      <label class="f"><span>Documents on your machines</span><input class="in" type="number" id="c-invdocs" min="0" step="1000" value="${C.inventDocs}"></label></div>
      <p class="hint" style="margin-top:8px">The hosted run writes four as a preview. Give invented documents a share in a mix with <code>invented_share</code> in the recipe.</p>` : ""}
    ${actions(true, "Continue")}`;
  }
  function stepCompose() {
    return `<h2>Compose</h2>
    <p class="lede">Build one or more training mixes from the prepared corpus. The default share is the static mix that won our 1B-token study: 50% textbook-style, 30% web, 20% educational web, with 10% each for math and code when present. It was measured at 70M parameters, so treat it as a starting point.</p>
    ${C.mixes.map((m, mi) => { const w = weightsOf(m), tot = Object.values(w).reduce((a, b) => a + b, 0) || 1;
      return `<div class="group mix"><div class="mixhead"><input class="in sm" id="c-mname-${mi}" value="${esc(m.name)}" aria-label="Mix name" spellcheck="false">
        <div class="seg">${BUDGETS.map(([l, v]) => `<button data-c="budget" data-m="${mi}" data-v="${v}" aria-pressed="${m.budget === v}">${l}</button>`).join("")}</div>
        <label class="hint">or <input class="in sm num wide" type="number" id="c-mbud-${mi}" value="${m.budget}" min="1000000" step="1000000"> tokens</label>
        ${C.mixes.length > 1 ? `<button class="linkish" data-c="rmmix" data-m="${mi}">Remove</button>` : ""}</div>
        <div class="wtable">${C.sources.map((s) => `<label><span>${esc(s.name)} <small class="hint">${KIND_NAMES[s.kind] || s.kind}</small></span>
          <input class="in sm num" type="number" min="0" max="100" step="1" id="c-w-${mi}-${esc(s.name)}" value="${Math.round((w[s.name] / tot) * 100)}"><span class="hint">%</span></label>`).join("")}</div></div>`; }).join("")}
    <p style="margin-top:14px"><button class="linkish" data-c="addmix">Add another mix</button> <span class="hint">for a different run, from the same prepared corpus</span></p>
    <div class="row2">${slider("c-epochs", "Repeat a source at most", C.maxEpochs, 1, 8, 0.5, (v) => `${v} times`)}
      ${slider("c-synth", "Synthetic text at most", C.maxSynth, 0, 0.6, 0.05, (v) => `${pct(v)} of a mix`)}</div>
    <p class="hint">A source short of its share is repeated first, then topped up by rephrasing its best documents. In our study a mix that leaned on synthetic-like text scored well on its own validation set but generalized worst.</p>
    ${actions(true, "Continue")}`;
  }

  /* ------------------------------------------------------------ run + report */
  const isFinal = (p) => p && ["done", "stopped", "failed"].includes(p.state);
  function hist(h) {
    const mx = Math.max(1, ...h);
    return `<div class="hist" aria-hidden="true">${h.map((v) => `<i style="height:${Math.round((v / mx) * 100)}%"></i>`).join("")}</div>`;
  }
  function report(rep) {
    const src = rep.sources || [], cal = (rep.stages || {}).calibrate || {}, comp = (rep.stages || {}).compose || {}, crt = (rep.stages || {}).create || {};
    const agree = cal.agreement || {};
    const drops = (f) => Object.entries(f.dropped).filter(([, v]) => v).sort((a, b) => b[1] - a[1]).map(([k, v]) => `${esc(k)} ${v}`).join(", ") || "none";
    return `
    <div class="group"><h3>What each source went through</h3><div class="tscroll"><table class="tbl"><thead><tr><th>Source</th><th>Read</th><th>Passed cleaning</th><th>Kept</th><th>Removed for test overlap</th><th>Tokens after curation</th></tr></thead><tbody>
      ${src.map((f) => `<tr><td><b>${esc(f.name)}</b><br><span class="hint">${drops(f)}</span></td><td>${f.read}</td><td>${f.kept}</td><td>${f.selected}</td>
        <td>${Object.keys(f.decontaminated).length ? esc(Object.entries(f.decontaminated).map(([k, v]) => `${BENCH_NAMES[k] || k} ${v}`).join(", ")) : "0"}</td>
        <td>${f.available_tokens ? "about " + fmt(f.available_tokens) : '<span class="hint">unknown size</span>'}</td></tr>`).join("")}</tbody></table></div>
      <p class="hint" style="margin-top:8px">Tokens after curation scale each sample's keep rate to the full dataset. They are estimates at four characters per token.</p></div>
    ${src.some((f) => f.repeated_passages && f.repeated_passages.length) ? `<div class="group"><h3>Passages repeated across documents</h3>${src.filter((f) => f.repeated_passages.length).map((f) => f.repeated_passages.slice(0, 3).map((r) => `<p class="quote"><b>${esc(f.name)}</b>, in ${r.docs} documents of the sample: ${esc(r.text)}</p>`).join("")).join("")}</div>` : ""}
    <div class="group"><h3>Scores</h3>
      <p class="hint">The model graded ${cal.graded} documents and those keep their grades. The rest are ranked by a scorer learned from so few grades that its ranking is close to a guess${cal.graded >= 100 ? ` (rank agreement with the model on held-out documents: quality ${agree.quality ?? "n/a"}${cal.has_targets ? `, relevance ${agree.relevance ?? "n/a"}` : ""})` : ""}. The full run grades ${C.gradeDocs.toLocaleString()} documents and reports how well the scorer agrees with the model.</p>
      <div class="hists">${src.map((f) => `<div><span>${esc(f.name)}</span>${hist(f.score_hist || [])}<small class="hint">low to high score</small></div>`).join("")}</div>
      ${src.map((f) => `<details class="ex"><summary>Best and worst kept in ${esc(f.name)}</summary>${[...(f.examples.top || []).map((e) => ["Best", e]), ...(f.examples.bottom || []).map((e) => ["Worst", e])].map(([l, e]) => `<div class="exd"><b>${l}</b> <span class="hint">score ${e.score}, quality ${e.quality}${cal.has_targets ? `, relevance ${e.relevance}` : ""}${e.reason ? ` / ${esc(e.reason)}` : ""}</span><div class="ptext">${esc(e.text)}</div></div>`).join("")}</details>`).join("")}</div>
    ${(rep.created_examples || []).length ? `<div class="group"><h3>Created</h3><p class="hint">${crt.kept} of ${crt.made} rewrites kept.${(crt.flagged || []).length ? ` ${crt.flagged.length} flagged for adding facts the source does not contain.` : ""}</p>
      ${rep.created_examples.map((c) => `<details class="ex"><summary>${esc(c.style ? (C.d.styles[c.style] || c.style) : "New document: " + (c.topic || ""))}${c.kept ? "" : ` <span class="flag">${esc(c.why)}</span>`}</summary>
        <div class="pairs two">${c.origin_text ? `<div class="pcard"><div class="ph"><b>Source</b></div><div class="ptext">${esc(c.origin_text)}</div></div>` : ""}<div class="pcard"><div class="ph"><b>Rewrite</b></div><div class="ptext">${esc(c.text)}</div></div></div></details>`).join("")}</div>` : ""}
    <div class="group"><h3>Mixes</h3>${(comp.plans || []).map((p) => `<p style="margin-top:12px"><b>${esc(p.name)}</b>, ${esc(p.budget)} tokens</p>
      <div class="tscroll"><table class="tbl"><thead><tr><th>Source</th><th>Share</th><th>Needs</th><th>Available</th><th>Repeats</th><th>Rephrased</th></tr></thead><tbody>
      ${p.rows.map((r) => `<tr><td>${esc(r.name)}</td><td>${pct(r.share)}</td><td>${fmt(r.want_tokens)}</td><td>${fmt(r.available_tokens)}</td><td>${r.epochs}x</td><td>${r.synthetic_tokens ? fmt(r.synthetic_tokens) : "0"}${r.short_tokens ? ` <span class="flag">${fmt(r.short_tokens)} short</span>` : ""}</td></tr>`).join("")}</tbody></table></div>
      ${p.synthetic_tokens ? `<p class="hint">About ${p.synthetic_calls.toLocaleString()} model calls to rephrase ${fmt(p.synthetic_tokens)} tokens on your endpoint.</p>` : ""}
      ${p.warnings.map((w) => `<p class="note">${esc(w)}</p>`).join("")}`).join("")}</div>
    ${(rep.warnings || []).map((w) => `<p class="note">${esc(w)}</p>`).join("")}`;
  }
  function stepRun() {
    const p = C.pv, fin = isFinal(p);
    const stageIdx = p ? ["setup", "clean", "calibrate", "create", "done"].indexOf(p.stage) : -1;
    const out = p && p.report && p.report.output;
    return `<h2>${!p ? "Run it on a sample" : !fin ? "Curating a sample" : p.state === "failed" ? "The run stopped" : "Your sample is curated"}</h2>
    <p class="lede">Every step runs here on about ${C.d ? C.d.per_source : 200} documents from each source. You get the kept and created documents, a report of what each step did, and the recipe to run the same steps on all of your data.</p>
    ${C.runErr ? `<p class="note bad">${esc(C.runErr)}</p>` : ""}
    ${!p ? `<div class="actions"><button class="btn" data-c="back">Back</button><button class="btn go" data-c="run">Run on a sample</button></div>` : ""}
    ${p && !fin ? `<ol class="stages">${["Reading", "Cleaning", "Calibrating", "Creating", "Composing"].map((s, i) => `<li class="${i < stageIdx ? "done" : i === stageIdx ? "on" : ""}">${s}</li>`).join("")}</ol>
      <p class="state">${esc(p.message || "Starting")}. Elapsed ${Math.round(p.elapsed || 0)} s.</p><div class="actions"><button class="btn" data-c="stop">Stop</button></div>` : ""}
    ${p && p.state === "failed" ? `<p class="note bad">${esc(p.message)}</p><div class="actions"><button class="btn" data-c="again">Change the recipe</button><button class="btn go" data-c="run">Try again</button></div>` : ""}
    ${p && fin && p.report ? `<div class="stats"><div><b>${out.docs}</b><span>documents ready</span></div><div><b>${out.curated}</b><span>kept from sources</span></div><div><b>${out.created}</b><span>created</span></div><div><b>${fmt(out.tokens)}</b><span>tokens, about</span></div></div>
      <div class="actions"><a class="btn go" href="/api/curate/previews/${p.id}/download" style="text-decoration:none">Download the sample (JSONL)</a><button class="btn" data-c="recipe">Download recipe</button><button class="btn" data-c="again">Change the recipe</button></div>
      ${runYourself()}${report(p.report)}` : ""}`;
  }
  function runYourself() {
    const inst = C.d ? C.d.install : "pip install datainventor";
    return `<div class="group"><h3>Run it on all of your data</h3>
      <p class="hint" style="max-width:62ch">On your own machines, the same recipe reads every document, grades ${C.gradeDocs.toLocaleString()} with your model, and writes the curated corpus and each mix as JSONL shards with a report. Point it at any OpenAI-compatible server (vLLM, SGLang, Ollama or a hosted API) by editing <code>endpoint</code> in the recipe. Interrupted runs resume where they stopped.</p>
      <pre class="code">${esc(inst)}
export DI_API_KEY=...          # key for the endpoint in the recipe
datainventor preview recipe.yaml          # optional: the hosted check, locally
datainventor run recipe.yaml --out ./curated --procs 32 --workers 64</pre></div>`;
  }

  /* ------------------------------------------------------------ render + events */
  function render() {
    if (window.DI_MODE !== "curate") return;
    ledger(); sheet();
    const fn = [stepSources, stepClean, stepCalibrate, stepCreate, stepCompose, stepRun][C.step];
    const keep = document.activeElement && document.activeElement.id;
    $("#panel").innerHTML = fn();
    if (keep && keep.startsWith("c-")) { const el = document.getElementById(keep); if (el) { el.focus(); if (el.setSelectionRange && el.type === "text") { const l = el.value.length; try { el.setSelectionRange(l, l); } catch (e) { /* ok */ } } } }
  }
  function go(i) { C.step = i; C.maxStep = Math.max(C.maxStep, i); render(); $("#panel").focus({ preventScroll: true }); window.scrollTo({ top: 0 }); }

  async function addDataset(id, role, kind) {
    C.addBusy = true; C.addErr = ""; render();
    try {
      const info = await api(`/api/curate/inspect?dataset=${encodeURIComponent(id)}`);
      const list = role === "target" ? C.targets : C.sources, max = role === "target" ? (C.d ? C.d.max_targets : 2) : (C.d ? C.d.max_sources : 4);
      if (list.length >= max) throw new Error(`The hosted run takes up to ${max} ${role}s. The recipe takes any number.`);
      let name = info.dataset.split("/")[1]; const taken = new Set([...C.sources, ...C.targets].map((s) => s.name));
      for (let k = 2; taken.has(name); k++) name = `${info.dataset.split("/")[1]}-${k}`;
      list.push({ ...info, name, kind: kind || info.kind });
      C.add = ""; C.pv = null;
    } catch (e) { C.addErr = e.message; }
    C.addBusy = false; render();
  }
  async function download() {
    try {
      const r = await fetch("/api/curate/recipe", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ recipe: recipe() }) });
      if (!r.ok) { const d = await r.json().catch(() => ({})); throw new Error(d.detail || "Could not build the recipe"); }
      const url = URL.createObjectURL(await r.blob()), a = document.createElement("a");
      a.href = url; a.download = "recipe.yaml"; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(url), 2000);
    } catch (e) { C.runErr = e.message; render(); }
  }
  async function run() {
    C.runErr = ""; C.pv = null;
    try { const r = await api("/api/curate/previews", { recipe: recipe() }); C.pid = r.id; C.pv = { id: r.id, state: "queued", stage: "setup", message: "Starting" }; render(); poll(); }
    catch (e) { C.runErr = e.message; render(); }
  }
  async function poll() {
    const id = C.pid; if (!id) return;
    try {
      const p = await api(`/api/curate/previews/${id}`);
      if (id !== C.pid) return;
      C.pv = p; if (C.step === 5) render();
      if (!isFinal(p)) setTimeout(poll, 1500);
    } catch (e) { C.pv = { id, state: "failed", message: e.message }; if (C.step === 5) render(); }
  }
  async function tryIt() {
    C.tryBusy = true; C.tryErr = ""; C.tryRes = null; render();
    const kind = (C.sources.find((s) => s.name === C.tryFrom) || {}).kind || "web";
    try { C.tryRes = await api("/api/curate/try", { text: C.tryText, kind, styles: C.styles.slice(0, 4) }); } catch (e) { C.tryErr = e.message; }
    C.tryBusy = false; render();
  }

  document.addEventListener("click", async (ev) => {
    if (window.DI_MODE !== "curate") return;
    const t = ev.target.closest("button, a"); if (!t) return;
    if (t.dataset.cgo !== undefined) return go(+t.dataset.cgo);
    if (t.dataset.bench) { const b = t.dataset.bench; C.decon = C.decon.includes(b) ? C.decon.filter((x) => x !== b) : [...C.decon, b]; return render(); }
    if (t.dataset.style) { const s = t.dataset.style; C.styles = C.styles.includes(s) ? C.styles.filter((x) => x !== s) : [...C.styles, s]; return render(); }
    const a = t.dataset.c; if (!a) return;
    if (a === "next") return go(C.step + 1);
    if (a === "back") return go(Math.max(0, C.step - 1));
    if (a === "add") return addDataset(C.add.trim(), C.addRole);
    if (a === "preset") { for (const p of PRESET) await addDataset(p.dataset, "source", p.kind); if (!C.targetsDesc) C.targetsDesc = ""; return render(); }
    if (a === "rm") { (t.dataset.role === "target" ? C.targets : C.sources).splice(+t.dataset.i, 1); C.pv = null; return render(); }
    if (a === "near") { C.near = !C.near; return render(); }
    if (a === "reph") { C.rephraseOn = !C.rephraseOn; return render(); }
    if (a === "inv") { C.inventOn = !C.inventOn; return render(); }
    if (a === "tryfill") { const s = C.sources.find((x) => x.preview); if (s) { C.tryText = s.preview; C.tryFrom = s.name; } return render(); }
    if (a === "try") return tryIt();
    if (a === "budget") { C.mixes[+t.dataset.m].budget = +t.dataset.v; return render(); }
    if (a === "addmix") { C.mixes.push({ name: `mix-${C.mixes.length + 1}`, budget: 1e9, weights: {} }); return render(); }
    if (a === "rmmix") { C.mixes.splice(+t.dataset.m, 1); return render(); }
    if (a === "recipe") return download();
    if (a === "run") return run();
    if (a === "stop") { if (C.pid) await api(`/api/curate/previews/${C.pid}/stop`, {}); return; }
    if (a === "again") { C.pv = null; C.pid = null; return go(0); }
  });
  document.addEventListener("input", (ev) => {
    if (window.DI_MODE !== "curate") return;
    const el = ev.target, id = el.id || ""; if (!id.startsWith("c-")) return;
    const v = el.value;
    if (id === "c-add") { C.add = v; const b = $('[data-c="add"]'); if (b) b.disabled = C.addBusy || !v.trim(); return; }
    if (id === "c-tdesc") { C.targetsDesc = v; return sheet(); }
    if (id === "c-try") { C.tryText = v; C.tryFrom = ""; const b = $('[data-c="try"]'); if (b) b.disabled = C.tryBusy || v.trim().split(/\s+/).length < 60; return; }
    if (id === "c-invreq") { C.inventReq = v; return; }
    const sv = (x) => { const o = document.getElementById(id + "-v"); if (o) o.textContent = x; };
    if (id === "c-focus") { C.focus = +v; sv(+v === 0 ? "general quality only" : `${pct(1 - v)} quality / ${pct(v)} targets`); return sheet(); }
    if (id === "c-keep") { C.keepTop = +v; sv(`${pct(v)} of each source`); return sheet(); }
    if (id === "c-epochs") { C.maxEpochs = +v; sv(`${v} times`); return; }
    if (id === "c-synth") { C.maxSynth = +v; sv(`${pct(v)} of a mix`); return; }
  });
  document.addEventListener("change", (ev) => {
    if (window.DI_MODE !== "curate") return;
    const el = ev.target, id = el.id || ""; if (!id.startsWith("c-")) return;
    const v = el.value; let m;
    if (id === "c-addrole") C.addRole = v;
    else if ((m = id.match(/^c-kind-(\d+)$/))) { C.sources[+m[1]].kind = v; C.pv = null; }
    else if ((m = id.match(/^c-col-(source|target)-(\d+)$/))) { (m[1] === "target" ? C.targets : C.sources)[+m[2]].text_field = v; C.pv = null; }
    else if ((m = id.match(/^c-rule-(.+)$/))) { if (v !== "") C.rules[m[1]] = +v; }
    else if (id === "c-grade") C.gradeDocs = Math.max(100, +v || 2000);
    else if (id === "c-scorer") C.scorer = v;
    else if (id === "c-invdocs") C.inventDocs = Math.max(0, +v || 0);
    else if ((m = id.match(/^c-mname-(\d+)$/))) C.mixes[+m[1]].name = v.trim().replace(/[^\w.-]+/g, "-") || `mix-${+m[1] + 1}`;
    else if ((m = id.match(/^c-mbud-(\d+)$/))) C.mixes[+m[1]].budget = Math.max(1e6, Math.round(+v || 0));
    else if ((m = id.match(/^c-w-(\d+)-(.+)$/))) { const mix = C.mixes[+m[1]]; mix.weights = { ...weightsOf(mix), [m[2]]: Math.max(0, +v || 0) / 100 }; }
    else return;
    render();
  });
  document.addEventListener("keydown", (ev) => { if (window.DI_MODE === "curate" && ev.key === "Enter" && ev.target.id === "c-add" && C.add.trim()) addDataset(C.add.trim(), C.addRole); });

  /* ------------------------------------------------------------ mode switch */
  const TAGS = {
    invent: "Describe the data you wish existed. Get training rows back. Bring any OpenAI-compatible model.",
    curate: "Clean, score, rephrase and mix the pretraining data you already have. Preview it here on a sample, then run the recipe on your own machines.",
    visualize: "Ask a question about a dataset and get a chart back, with the rows behind every bar one click away.",
  };
  function setMode(mode) {
    window.DI_MODE = mode;
    document.querySelectorAll("[data-mode]").forEach((b) => b.setAttribute("aria-pressed", String(b.dataset.mode === mode)));
    $(".tag").textContent = TAGS[mode] || TAGS.invent;
    if (mode === "curate") { if (!C.d) api("/api/curate/defaults").then((d) => { C.d = d; render(); }).catch(() => render()); render(); }
    else if (mode === "visualize") { if (window.DI_renderVis) window.DI_renderVis(); }
    else if (window.DI_renderInvent) window.DI_renderInvent();
    const hash = mode === "invent" ? "" : "#" + mode;
    if (location.hash !== hash) history.replaceState(null, "", hash || location.pathname + location.search);
  }
  window.DI_setMode = setMode;
  document.addEventListener("click", (ev) => { const b = ev.target.closest("[data-mode]"); if (b) setMode(b.dataset.mode); });
  window.DI_MODE = location.hash === "#curate" ? "curate" : location.hash === "#visualize" ? "visualize" : "invent";
  if (window.DI_MODE !== "invent") document.addEventListener("DOMContentLoaded", () => setMode(window.DI_MODE));
})();