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]>
395 lines
25 KiB
JavaScript
395 lines
25 KiB
JavaScript
// 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 };
|
||
}
|