open-standards/wai/quantum-studio/src/workbench.js
Transaction Science f090e8b6d1 quantum-studio: the course in the Studio and the desktop app, and the interface in Simplified Chinese
The Course pane lists the 21 lessons, shows a lesson's text and exercise,
loads its starter, and checks the program in the editor with the engine
(engine_course_check in the page, the same commands in the desktop app):
each case's verdict, each rule, What went wrong? from the diagnostics, the
verdict and the critique, the hints one at a time with the engine's pointer,
and a hint from the assistant when a model is connected, any program in its
answer held back. Progress is kept only in this browser and says so.

The interface's words are data, locales/<locale>/<area>.json for every part,
English the source and Simplified Chinese marked machine-assisted; the Studio
follows the browser's language until the reader picks one with the switch.
test/locales.test.mjs holds every locale to every key and placeholder. The
e2e checks the course in English and in Chinese. wai-web's engine tests pin
23 native and 16 wasm32, a course check the same bytes in both.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 08:43:06 -04:00

395 lines
25 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// The Workbench: a family of circuits searched for its cheapest members, every
// proposal judged by the engine (engine::discover).
//
// The reader writes a generator in the editor (a program with integer inputs)
// and says what each member of its family must do (the same state from
// |0…0⟩, or the same circuit), at which sizes, with which gates and
// connectivity, and what it costs. The engine checks every proposal at every
// size, never a model's judgement, and the archive keeps the cheapest verified
// generator in every cell of two measures, with who proposed it, what it came
// from and what it tries. Proposers are the reader, the built-in blind search
// (the baseline any idea should beat) and the model, when one is connected;
// each is named on what it found. Nothing changes the editor until the reader
// opens an entry.
//
// What is new in a kept generator is said beside it: the known construction it
// is, or a known result reached by a different route, or how far it is from
// everything it was compared with. A candidate that breaks only one rule of
// the specification and would be verified without it is an aberration: never
// kept, it is offered as a reason to revise the rule. What is worth a second
// look goes to Found.
import { el } from './dom.js';
import { t } from './i18n.js';
import { declared } from './generators.js';
import { noveltyLabel, unaskedText } from './novelty.js';
import { tastePanel } from './taste.js';
const SAVED = 'wai.quantum-studio.workbench';
// A cost as the Studio shows it: a sum of weighted measures, to three places.
const num = (x) => String(Math.round(x * 1000) / 1000);
const NS = 'http://www.w3.org/2000/svg';
const svg = (tag, attrs = {}, ...kids) => {
const e = document.createElementNS(NS, tag);
for (const [k, v] of Object.entries(attrs)) if (v != null) e.setAttribute(k, String(v));
for (const k of kids) if (k != null) e.append(typeof k === 'string' ? document.createTextNode(k) : k);
return e;
};
// The measures a cost or an axis may name, with the words the Studio shows.
export const MEASURES = {
depth: t('workbench.measure_depth'),
two_qubit: t('workbench.measure_two_qubit'),
t_count: t('workbench.measure_t_count'),
gates: t('workbench.measure_gates'),
two_qubit_depth: t('workbench.measure_two_qubit_depth'),
one_qubit: t('workbench.measure_one_qubit'),
three_qubit: t('workbench.measure_three_qubit'),
non_clifford: t('workbench.measure_non_clifford'),
qubits: t('workbench.measure_qubits'),
};
// The sizes a reader writes ("n = 3, 5, 8", or "3, 5, 8" for a generator with
// one input, "n = 3, 5; m = 2" for two) as the specification's inputs, or why
// they cannot be read.
export function parseSizes(text, names) {
if (!names.length) return { inputs: {} };
const out = {};
const parts = text.split(';').map((p) => p.trim()).filter(Boolean);
for (const part of parts) {
const m = part.match(/^(?:([A-Za-z_]\w*)\s*=\s*)?(.+)$/);
const name = m[1] ?? (names.length === 1 ? names[0] : null);
if (!name) return { error: t('workbench.sizes_name_each', { list: names.map((n) => `${n} = 3, 5`).join('; ') }) };
if (!names.includes(name)) return { error: t('workbench.sizes_no_input', { name, names: names.join(', ') }) };
const values = m[2].split(/[\s,]+/).filter(Boolean).map(Number);
if (!values.length || values.some((v) => !Number.isInteger(v) || v < 0)) return { error: t('workbench.sizes_whole', { name }) };
out[name] = [...new Set(values)].sort((a, b) => a - b);
}
const missing = names.filter((n) => !out[n]);
if (missing.length) return { error: t('workbench.sizes_missing', { names: missing.join(', ') }) };
return { inputs: out };
}
// The specification the form describes, for the program `reference`.
export function specOf({ reference, task, sizes, gates, connectivity, weights, axes }) {
const names = declared(reference);
const s = parseSizes(sizes, names);
if (s.error) return { error: s.error };
const cost = Object.fromEntries(Object.entries(weights).filter(([, w]) => Number(w) > 0).map(([k, w]) => [k, Number(w)]));
if (!Object.keys(cost).length) return { error: t('workbench.cost_needs_weight') };
const allowed = gates.split(/[\s,]+/).map((g) => g.trim().toLowerCase()).filter(Boolean);
const spec = { name: 'studio', task, reference, inputs: s.inputs, connectivity, cost, axes };
if (allowed.length) spec.gates = allowed;
return { spec };
}
// The archive's elites on the two axes: a ring for the reference, a dot for
// each other cell's best, the cheapest marked. A click on a dot picks it.
export function archiveMap(summary, axes, selected, onPick) {
const W = 320, H = 190, L = 34, B = 26, T = 22, R = 12;
const cells = summary.cells ?? [];
const xs = cells.map((c) => c.axes[0]), ys = cells.map((c) => c.axes[1]);
const xmax = Math.max(1, ...xs) * 1.15, ymax = Math.max(1, ...ys) * 1.15;
const sx = (v) => L + (v / xmax) * (W - L - R), sy = (v) => H - B - (v / ymax) * (H - B - T);
const ticks = (max) => { const step = Math.max(1, Math.ceil(max / 4)); const out = []; for (let v = 0; v <= max; v += step) out.push(v); return out; };
const g = svg('svg', { class: 'qs-atlas qs-wb-map', id: 'qs-wb-map', viewBox: `0 0 ${W} ${H}`, role: 'img', 'aria-label': t('workbench.map_label', { n: cells.length, x: MEASURES[axes[0]] ?? axes[0], y: MEASURES[axes[1]] ?? axes[1] }) });
for (const v of ticks(xmax)) g.append(svg('line', { x1: sx(v), y1: T, x2: sx(v), y2: H - B, class: 'qs-gridline' }), svg('text', { x: sx(v), y: H - B + 13, class: 'qs-tick', 'text-anchor': 'middle' }, String(v)));
for (const v of ticks(ymax)) g.append(svg('line', { x1: L, y1: sy(v), x2: W - R, y2: sy(v), class: 'qs-gridline' }), svg('text', { x: L - 6, y: sy(v) + 3, class: 'qs-tick', 'text-anchor': 'end' }, String(v)));
g.append(svg('text', { x: W - R, y: H - 2, class: 'qs-axis', 'text-anchor': 'end' }, `${MEASURES[axes[0]] ?? axes[0]} →`),
svg('text', { x: L - 6, y: 10, class: 'qs-axis', 'text-anchor': 'start' }, `↑ ${MEASURES[axes[1]] ?? axes[1]}`));
const best = summary.best?.id;
const kindOf = (c) => (c.proposer === 'reference' ? 'ref' : c.id === best ? 'best' : c.proposer.startsWith('model') ? 'model' : c.proposer === 'mutate' ? 'mutate' : 'person');
// Drawn so the reference and then the best lie on top where points crowd.
const rank = { mutate: 0, model: 1, person: 1, ref: 2, best: 3 };
for (const c of [...cells].sort((a, b) => rank[kindOf(a)] - rank[kindOf(b)])) {
const kind = kindOf(c);
const p = svg('g', { class: `qs-pt qs-wb-${kind}${c.id === selected ? ' qs-sel' : ''}`, tabindex: '0', role: 'button', 'data-id': c.id, 'aria-label': t('workbench.point_label', { id: c.id, proposer: c.proposer, cost: num(c.cost), x: c.axes[0], y: c.axes[1] }) },
svg('title', {}, t('workbench.point_title', { id: c.id, proposer: c.proposer, cost: num(c.cost) })),
svg('circle', { cx: sx(c.axes[0]), cy: sy(c.axes[1]), r: 8, class: 'qs-hit' }),
svg('circle', { cx: sx(c.axes[0]), cy: sy(c.axes[1]), r: kind === 'ref' ? 6 : 4.5, class: kind === 'ref' ? 'qs-ring' : 'qs-dot' }));
p.addEventListener('click', () => onPick(c.id));
p.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onPick(c.id); } });
g.append(p);
}
return g;
}
// One line for an outcome of a submission.
export function outcomeText(o, who) {
if (o.duplicate) return t('workbench.outcome_duplicate', { who });
const e = o.evaluation;
if (!e?.verified) return t('workbench.outcome_not_verified', { who, failure: e?.failure ?? t('workbench.outcome_did_not_read') });
if (o.kept && o.improvement) return t('workbench.outcome_improvement', { who, id: o.kept, cost: num(e.cost) });
if (o.kept) return t('workbench.outcome_kept', { who, id: o.kept, cost: num(e.cost) });
return t('workbench.outcome_verified', { who, cost: num(e.cost) });
}
// Who a proposer is, as the program's history records it.
const byOf = (p) => (p === 'mutate' ? { kind: 'engine', name: 'the blind search' }
: p === 'person' || p === 'reference' ? { kind: 'person' }
: p.startsWith('model') ? { kind: 'assistant', name: p.replace(/^model:?\s*/, '') || undefined } : { kind: 'agent', name: p });
// `save(name, text)` writes a file where the reader keeps them and gives back
// where it went. `apply(source, note, by)` offers a generator to the editor as
// `by`'s proposal. `found` is the Found pane; `taste` the reader's taste
// profiles (taste.js), learned from choices between kept generators.
// `paused()` is true while the reader explores on their own: the model is not asked.
// `features.on(name)` says whether a feature is on (flags.js); `onChoice` hears
// which kept generator the reader opened, by its rank in cost.
export function workbenchPane({ engine, draft, model, apply, save, found = null, taste = null, paused = () => false, features = { on: () => true }, onChoice = null }) {
const startBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-wb-start', type: 'button', text: t('workbench.start'), title: t('workbench.start_title') });
const task = el('select', { class: 'qs-input qs-small', id: 'qs-wb-task', 'aria-label': t('workbench.task_label') },
el('option', { value: 'prepares', text: t('workbench.task_prepares') }), el('option', { value: 'equivalent', text: t('workbench.task_equivalent') }));
const sizes = el('input', { class: 'qs-input', id: 'qs-wb-sizes', value: 'n = 3, 5, 8', autocomplete: 'off', 'aria-label': t('workbench.sizes_label') });
const gates = el('input', { class: 'qs-input', id: 'qs-wb-gates', placeholder: t('workbench.gates_any'), value: 'h, cx', autocomplete: 'off', 'aria-label': t('workbench.gates_label') });
const conn = el('select', { class: 'qs-input qs-small', id: 'qs-wb-conn', 'aria-label': t('workbench.conn_label') },
el('option', { value: 'all', text: t('workbench.conn_all') }), el('option', { value: 'line', text: t('workbench.conn_line') }));
const weight = (k, v) => el('input', { class: 'qs-input qs-small qs-wb-w', type: 'number', min: '0', step: '0.1', value: String(v), id: `qs-wb-w-${k}`, 'aria-label': t('workbench.weight_label', { measure: MEASURES[k] }) });
const weights = { depth: weight('depth', 1), two_qubit: weight('two_qubit', 0.1), t_count: weight('t_count', 0), gates: weight('gates', 0) };
const axis = (id, chosen) => el('select', { class: 'qs-input qs-small', id, 'aria-label': id.endsWith('x') ? t('workbench.axis_first') : t('workbench.axis_second') },
...Object.entries(MEASURES).map(([k, w]) => el('option', { value: k, text: w, selected: k === chosen })));
const ax = axis('qs-wb-ax', 'two_qubit'), ay = axis('qs-wb-ay', 'depth');
const form = el('details', { class: 'qs-wb-spec', id: 'qs-wb-spec', open: true }, el('summary', { text: t('workbench.spec') }),
el('div', { class: 'qs-wb-form' },
el('label', {}, el('span', { text: t('workbench.each_member_must') }), task),
el('label', {}, el('span', { text: t('workbench.checked_at') }), sizes),
el('label', {}, el('span', { text: t('workbench.using') }), gates),
el('label', {}, el('span', { text: t('workbench.on') }), conn),
el('div', { class: 'qs-wb-cost' }, el('span', { text: t('workbench.cost_summed') }),
...Object.entries(weights).map(([k, i]) => el('label', {}, i, el('span', { text: `× ${MEASURES[k]}` })))),
el('label', {}, el('span', { text: t('workbench.map') }), ax, el('span', { text: t('workbench.against') }), ay)));
const searchBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-wb-search', type: 'button', text: t('workbench.search'), title: t('workbench.search_title') });
const askBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-wb-ask', type: 'button', text: t('workbench.ask'), title: t('workbench.ask_title') });
const submitBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-wb-submit', type: 'button', text: t('workbench.submit'), title: t('workbench.submit_title') });
const saveBtn = el('button', { class: 'qs-btn qs-small', id: 'qs-wb-save', type: 'button', text: t('workbench.download'), title: t('workbench.download_title') });
const acts = el('div', { class: 'qs-wb-acts', hidden: true }, searchBtn, askBtn, submitBtn, saveBtn);
const stats = el('p', { class: 'qs-note', id: 'qs-wb-stats', text: t('workbench.intro') });
const mapBox = el('div', { class: 'qs-wb-mapbox', id: 'qs-wb-mapbox' });
const entry = el('div', { class: 'qs-wb-entry', id: 'qs-wb-entry' });
const log = el('ol', { class: 'qs-wb-log', id: 'qs-wb-log', 'aria-live': 'polite' });
let spec = null, archive = null, summary = null, selected = null, openedFrom = null, busy = false;
// The kept generators, each the best of its cell, read at the largest size.
const largest = () => Object.fromEntries(Object.entries(spec?.inputs ?? {}).map(([k, vs]) => [k, Math.max(...vs)]));
const elites = () => (archive ? Object.values(archive.cells).map((i) => archive.entries[i]).sort((a, b) => a.cost - b.cost).map((e) => ({ name: e.id, qasm: e.source, cost: e.cost })) : null);
const tasteUi = taste ? tastePanel({ store: taste, candidates: elites, inputs: largest, task: () => spec?.task ?? 'equivalent', onPick: (id) => pick(id) }) : null;
const node = el('section', { class: 'qs-pane qs-wb', 'aria-labelledby': 'qs-wb-h' },
el('div', { class: 'qs-head' }, el('h2', { id: 'qs-wb-h', text: t('workbench.title') }), el('span', { class: 'qs-grow' }), startBtn),
...[form, acts, stats, mapBox, entry, tasteUi?.node, log].filter(Boolean));
const say = (text, cls = '', ...more) => { log.prepend(el('li', { class: cls }, text, ...more)); while (log.children.length > 12) log.lastChild.remove(); };
const persist = () => { try { localStorage.setItem(SAVED, JSON.stringify({ spec, archive })); } catch (e) {} };
const setBusy = (b) => { busy = b; for (const x of [startBtn, searchBtn, askBtn, submitBtn]) x.disabled = b; };
const entryById = (id) => archive?.entries.find((e) => e.id === id);
function render() {
if (!summary) return;
const st = summary.stats;
const energy = st.joules != null ? t('workbench.stats_energy', { joules: st.joules.toFixed(2) }) : '';
const improvements = st.improvements === 1 ? t('workbench.improvement_one', { n: st.improvements }) : t('workbench.improvement_many', { n: st.improvements });
stats.textContent = t('workbench.stats', { evaluations: st.evaluations, verified: st.verified, kept: st.kept, improvements, energy })
+ t('workbench.stats_best', { id: summary.best.id, cost: num(summary.best.cost), reference: num(summary.reference.cost), proposer: summary.best.proposer, strength: summary.best.strength });
mapBox.replaceChildren(archiveMap(summary, spec.axes, selected, pick));
acts.hidden = false;
if (selected) pick(selected);
}
// The form, showing the specification an archive was built for.
function fill(sp) {
task.value = sp.task;
sizes.value = Object.entries(sp.inputs ?? {}).map(([n, vs]) => `${n} = ${vs.join(', ')}`).join('; ');
gates.value = (sp.gates ?? []).join(', ');
conn.value = sp.connectivity ?? 'all';
for (const [k, i] of Object.entries(weights)) i.value = String(sp.cost?.[k] ?? 0);
[ax.value, ay.value] = sp.axes;
}
function lineage(id) {
const out = [];
for (let e = entryById(id); e && out.length < 20; e = e.parent ? entryById(e.parent) : null) out.push(e);
return out;
}
function pick(id) {
const e = entryById(id);
if (!e) return;
selected = id;
for (const p of mapBox.querySelectorAll('.qs-pt')) p.classList.toggle('qs-sel', p.dataset.id === id);
const open = el('button', { class: 'qs-btn qs-small', id: 'qs-wb-open', type: 'button', text: t('workbench.open') });
open.addEventListener('click', () => {
openedFrom = id;
const ranked = elites() ?? [];
if (ranked.length >= 2) onChoice?.({ kind: 'workbench', k: ranked.length, picked: Math.max(0, ranked.findIndex((x) => x.name === id)), best: 0 });
apply(e.source, `${id} from the Workbench (${e.proposer}).`, byOf(e.proposer));
});
const chain = lineage(id).map((x, i) => el('button', { class: 'qs-chip', type: 'button', text: `${i ? '← ' : ''}${x.id} ${x.proposer}`, title: x.note ?? '', onclick: () => pick(x.id) }));
const novel = el('div', { class: 'qs-wb-novelty', id: 'qs-wb-novelty' });
(features.on('novelty') ? engine.discoverNovelty?.(spec, archive, id) : null)?.then((v) => {
if (selected !== id) return;
const nv = noveltyLabel(v, { verified: true });
const u = unaskedText(v, MEASURES);
novel.replaceChildren(...[nv ? el('span', { class: `qs-badge ${nv.cls}`, text: nv.text }) : null,
nv ? el('p', { class: 'qs-note', text: nv.why }) : null, u ? el('p', { class: 'qs-note', text: u }) : null].filter(Boolean));
}).catch(() => {});
entry.replaceChildren(...[
el('div', { class: 'qs-cand-head' }, el('span', { class: 'qs-cand-title', text: t('workbench.entry_title', { id: e.id, cost: num(e.cost), x_measure: MEASURES[spec.axes[0]], x_value: e.axes[0], y_measure: MEASURES[spec.axes[1]], y_value: e.axes[1] }) }),
el('span', { class: 'qs-badge ok', text: t('workbench.checked_by', { strength: e.strength, sealed: e.sealed ? t('workbench.sealed') : '' }) }), el('span', { class: 'qs-grow' }), open),
e.note ? el('p', { class: 'qs-wb-note', text: e.note }) : null,
novel,
el('div', { class: 'qs-wb-chain', 'aria-label': t('workbench.lineage') }, ...chain),
el('details', {}, el('summary', { text: t('workbench.its_generator') }), el('pre', { class: 'qs-ai-code', text: e.source })),
].filter(Boolean));
}
async function start() {
if (busy) return;
const reference = draft();
const made = specOf({ reference, task: task.value, sizes: sizes.value, gates: gates.value, connectivity: conn.value, weights: Object.fromEntries(Object.entries(weights).map(([k, i]) => [k, i.value])), axes: [ax.value, ay.value] });
if (made.error) { stats.textContent = made.error; return; }
setBusy(true);
stats.textContent = t('workbench.checking_reference');
try {
const r = await engine.discoverStart(made.spec);
({ archive, summary } = r);
spec = made.spec;
selected = null; openedFrom = null;
log.replaceChildren();
say(t('workbench.started', { id: summary.reference.id, cost: num(summary.reference.cost) }));
form.open = false;
persist();
render();
} catch (e) { stats.textContent = e.message.charAt(0).toUpperCase() + e.message.slice(1); }
finally { setBusy(false); }
}
async function submit(source, parent, proposer, note) {
const r = await engine.discoverSubmit(spec, archive, source, parent, proposer, note);
({ archive, summary } = r);
return r.outcome;
}
// The form loosened by the rule an aberration broke, for a new archive.
function loosen(broke) {
if (broke.kind === 'gates' && broke.gate) gates.value = [...new Set([...gates.value.split(/[\s,]+/).filter(Boolean), broke.gate])].join(', ');
else if (broke.kind === 'connectivity') conn.value = 'all';
form.open = true;
stats.textContent = t('workbench.loosened', { change: broke.kind === 'gates' ? t('workbench.loosened_allows', { gate: broke.gate }) : t('workbench.loosened_any_pair') });
startBtn.focus();
}
// What is new in a kept entry, said and flagged when it is worth a second look.
async function novelty(id, who, by) {
if (!features.on('novelty')) return;
const v = await engine.discoverNovelty?.(spec, archive, id).catch(() => null);
const nv = noveltyLabel(v, { verified: true });
if (!nv) return;
say(t('workbench.novelty_said', { id, text: nv.text }), nv.flag ? 'find' : '');
if (nv.flag) found?.flag({ kind: nv.flag, title: `${id} (${who}): ${nv.text}`, why: nv.why, where: t('workbench.title'), source: entryById(id)?.source, by });
}
// What a judged candidate adds to its outcome: an aberration's invitation to
// revise a rule, and what is new in it once kept.
async function after(o, who, source, by) {
const ab = features.on('novelty') ? o.evaluation?.aberration : null;
if (ab) {
const broke = ab.broke?.kind === 'gates' ? t('workbench.aberration_uses', { gate: ab.broke.gate }) : ab.broke?.kind === 'connectivity' ? t('workbench.aberration_joins') : t('workbench.aberration_breaks');
const why = t('workbench.aberration_why', { rule: ab.rule, strength: ab.strength, cost: num(ab.cost), x_measure: MEASURES[spec.axes[0]], x_value: ab.axes[0], y_measure: MEASURES[spec.axes[1]], y_value: ab.axes[1] });
const tryBtn = ab.broke ? el('button', { class: 'qs-btn qs-small qs-wb-try', type: 'button', text: t('workbench.try_without'), onclick: () => loosen(ab.broke) }) : null;
say(t('workbench.aberration_said', { who, why }), 'find', tryBtn);
found?.flag({ kind: 'aberration', title: `${who}: ${broke}`, why, where: t('workbench.title'), source, by });
}
if (o.kept) await novelty(o.kept, who, by);
}
async function searchMore() {
if (busy || !spec) return;
setBusy(true);
const total = 100, chunk = 25;
const before = summary.stats.kept;
const kept = [];
try {
for (let done = 0; done < total; done += chunk) {
stats.textContent = t('workbench.search_progress', { done, total });
// A fresh seed for every step, so a search taken up again does not replay.
const r = await engine.discoverSearch(spec, archive, archive.stats.evaluations + archive.stats.duplicates + 1, chunk);
({ archive, summary } = r);
kept.push(...(r.kept ?? []));
}
say(t('workbench.search_done', { total, kept: summary.stats.kept - before }));
persist();
// Only what is worth a second look is said of the search's finds.
for (const id of features.on('novelty') ? kept : []) {
const nv = noveltyLabel(await engine.discoverNovelty?.(spec, archive, id).catch(() => null), { verified: true });
if (nv?.flag) { say(t('workbench.search_find_said', { id, text: nv.text }), 'find'); found?.flag({ kind: nv.flag, title: t('workbench.search_find_title', { id, text: nv.text }), why: nv.why, where: t('workbench.title'), source: entryById(id)?.source, by: byOf('mutate') }); }
}
} catch (e) { say(t('workbench.search_stopped', { error: e.message }), 'bad'); }
finally { setBusy(false); render(); }
}
async function ask() {
if (busy || !spec) return;
const m = model();
if (paused()) { say(t('workbench.paused'), 'bad'); return; }
if (!m) { say(t('workbench.needs_model'), 'bad'); return; }
setBusy(true);
stats.textContent = t('workbench.model_proposing');
try {
const shown = Object.values(archive.cells).map((i) => archive.entries[i]).sort((a, b) => a.cost - b.cost).slice(0, 3);
const ideas = await m.propose(spec, shown, 3);
if (!ideas.length) say(t('workbench.model_wrote_none'), 'bad');
for (const p of ideas) {
const proposer = `model: ${m.model?.name || 'the model'}`;
const o = await submit(p.qasm, shown[0]?.id ?? null, proposer, p.idea || null);
say(outcomeText(o, p.idea || t('workbench.an_idea')), o.kept ? 'ok' : o.evaluation?.verified ? '' : 'bad');
await after(o, p.idea || t('workbench.an_idea'), p.qasm, byOf(proposer));
}
persist();
} catch (e) { say(t('workbench.model_failed', { error: e.message }), 'bad'); }
finally { setBusy(false); render(); }
}
async function submitDraft() {
if (busy || !spec) return;
setBusy(true);
try {
const src = draft();
const parent = openedFrom && entryById(openedFrom)?.source !== src ? openedFrom : null;
const o = await submit(src, parent, 'person', null);
say(outcomeText(o, t('workbench.your_program')), o.kept ? 'ok' : o.evaluation?.verified ? '' : 'bad');
await after(o, t('workbench.your_program'), src, byOf('person'));
persist();
} catch (e) { say(t('workbench.not_judged', { error: e.message }), 'bad'); }
finally { setBusy(false); render(); }
}
// The specification and its archive, as the command line keeps them:
// `wai-quantum discover verify NAME.json` reads NAME.archive.json beside it.
async function download() {
if (!archive) return;
const stem = `workbench-${archive.spec.slice(0, 8)}-${archive.stats.evaluations}`;
try {
const where = await save(`${stem}.json`, JSON.stringify(spec, null, 2) + '\n');
await save(`${stem}.archive.json`, JSON.stringify(archive, null, 2) + '\n');
say(t('workbench.saved', { path: where, stem }), 'ok');
} catch (e) { say(t('workbench.not_saved', { error: e.message }), 'bad'); }
}
// The last archive comes back with the page, if the engine still reads it
// for its specification (a search of no evaluations changes nothing).
try {
const was = JSON.parse(localStorage.getItem(SAVED) || 'null');
if (was?.spec && was?.archive) {
engine.discoverSearch(was.spec, was.archive, 1, 0).then((r) => {
if (spec) return;
spec = was.spec; ({ archive, summary } = r); fill(spec); form.open = false;
say(t('workbench.resumed', { n: summary.stats.evaluations }));
render();
}).catch(() => {});
}
} catch (e) {}
startBtn.addEventListener('click', start);
searchBtn.addEventListener('click', searchMore);
askBtn.addEventListener('click', ask);
submitBtn.addEventListener('click', submitDraft);
saveBtn.addEventListener('click', download);
return { node, start, searchMore, ask, submitDraft };
}