A run on one of the person's providers waited inside the desktop app for up to two hours, holding the Studio, and then cancelled the job, giving up its place in a device's queue. The app now writes the job down before it sends it, with the command line's records of jobs on a provider (#581), and asks after it by its id. - wai-quantum-desktop: run_on_provider answers within ten seconds, done or still running with its id; provider_job asks after a job once or cancels it; provider_jobs lists the jobs sent, asking no provider. - Quantum Studio: while a job waits, Cancel cancels it at the provider and "Wait in the background" leaves it there and frees the Studio. A job left waiting, from this window or an earlier one, is followed while the app is open, and its result goes to the history when it finishes. Your jobs lists every job sent. A check that fails during the wait does not end it. - The tests: the app's queued job answered by its id, sealed with the app's key, the same answer after, and cancelled only when asked; the page's wait, cancel, cancel-after-finish, failed check, background and reopened window. The studio workflow names the new app test on every leg. Co-Authored-By: Claude Opus 5.5 <[email protected]>
1082 lines
71 KiB
JavaScript
1082 lines
71 KiB
JavaScript
// Quantum Studio: an editor for OpenQASM programs, run on this device or on
|
||
// the quantum job service by the same engine. Every result names its method,
|
||
// why, and how accurate it is, and carries a receipt checked here. One
|
||
// interface for the browser and the desktop app.
|
||
//
|
||
// The engine is the microscope and the judge: it critiques the draft as it is
|
||
// written, measures and checks every alternative, and answers every run. The
|
||
// assistant, a model on this machine, is an untrusted muse that speaks only when
|
||
// asked. The reader is the author: nothing changes the program until they apply it,
|
||
// and a proposal is taken change by change. Every version is recorded with who
|
||
// made it, and a run's receipt binds whose hand each gate is in.
|
||
//
|
||
// The host gives a mount element:
|
||
// <div id="studio" data-engine-pkg="…/wai_quantum_ide.js" data-worker="…/worker.js"
|
||
// data-service="…/quantum-jobs"></div>
|
||
// and, on the desktop, window.__TAURI__ (the native engine).
|
||
import { EditorView, keymap, lineNumbers, highlightActiveLine, highlightActiveLineGutter, drawSelection, gutter, GutterMarker } from '@codemirror/view';
|
||
import { EditorState, Compartment, StateEffect, StateField, RangeSet } from '@codemirror/state';
|
||
import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands';
|
||
import { syntaxHighlighting, HighlightStyle, bracketMatching, indentOnInput } from '@codemirror/language';
|
||
import { tags as t } from '@lezer/highlight';
|
||
import { linter, lintGutter, forceLinting } from '@codemirror/lint';
|
||
import { autocompletion, closeBrackets, closeBracketsKeymap, completionKeymap } from '@codemirror/autocomplete';
|
||
import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
|
||
import { openqasm, complete } from './qasm-lang.js';
|
||
import { TabEngine, ServiceEngine, DesktopEngine, onDesktop } from './engine.js';
|
||
import { levelBadge, verdictLine } from './verdict-view.js';
|
||
import { EXAMPLES } from './examples.js';
|
||
import * as History from './history.js';
|
||
import { circuitSvg, mark, annotate } from './circuit-svg.js';
|
||
import { Assistant, HttpModel, DesktopModel, STANCES } from './assistant.js';
|
||
import { el, sameness } from './dom.js';
|
||
import { t as tr, locale, NAMES, LOCALES, chooseAndReload, machineAssisted } from './i18n.js';
|
||
import { coursePane } from './course.js';
|
||
import { critiquePanel } from './critique-view.js';
|
||
import { galleryPane } from './gallery.js';
|
||
import { workbenchPane } from './workbench.js';
|
||
import { foundPane } from './novelty.js';
|
||
import { tasteStore } from './taste.js';
|
||
import { organicMode, convergePanel, ORGANIC } from './converge.js';
|
||
import { featureFlags, flagsView } from './flags.js';
|
||
import { studyLog, studyPane } from './study.js';
|
||
import { versionsPane, HANDS, whoName } from './versions.js';
|
||
import { rulesPane, heldLines, LOCK, UNLOCK } from './rules.js';
|
||
import { declared, inputsBar, withInputs } from './generators.js';
|
||
import { shortcutsPanel } from './shortcuts-view.js';
|
||
import { rewritePanel } from './rewrite-view.js';
|
||
import { compilePanel } from './compile-view.js';
|
||
import { estimatePanel } from './estimate-view.js';
|
||
import { qecPane } from './qec-view.js';
|
||
import { pulsePane } from './pulse-view.js';
|
||
import * as Predict from './predict.js';
|
||
import { HardwareEngine, askDialog, hardwareSegment, jobsDialog } from './hardware.js';
|
||
import { debuggerPane } from './debugger.js';
|
||
import { circuitEditor } from './visual-edit.js';
|
||
import { devicePane, deviceBlock } from './device.js';
|
||
import { METHOD_NAMES, ACCURACY, compact, accuracyExtras, countsBlock, expectationsBlock, amplitudesBlock, energyText } from './result-parts.js';
|
||
import { answerRenderer, answerRecord } from './answer-view.js';
|
||
import { telemetryStore, telemetryPanel } from './telemetry.js';
|
||
import { Updates, updateBar } from './update.js';
|
||
|
||
const NOISE = ['depolarizing_1q', 'depolarizing_2q', 'amplitude_damping', 'dephasing', 'readout'].map((k) => [k, tr(`noise.${k}`)]);
|
||
const MITIGATE = [['none', 'none'], ['zne', 'zero-noise extrapolation'], ['pea', 'error amplification (learned noise)'], ['pec', 'error cancellation (learned noise)'], ['cdr', 'Clifford data regression'], ['vd', 'virtual distillation'], ['readout', 'readout inversion']];
|
||
|
||
// A job's mitigation: each value raw and mitigated with its standard error, the
|
||
// ideal one the simulation knows, the method's assumptions and its cost.
|
||
function mitigationBlock(m) {
|
||
const n = (x, d = 5) => (x == null ? '—' : Number(x).toFixed(d));
|
||
const rows = (m.observables ?? []).map((o) => el('tr', {},
|
||
el('th', { scope: 'row', text: `⟨${o.observable}⟩` }),
|
||
el('td', { class: 'num', text: `${n(o.raw)} ± ${n(o.raw_stderr, 4)}` }),
|
||
el('td', { class: 'num', text: `${n(o.mitigated)} ± ${n(o.stderr, 4)}` }),
|
||
el('td', { class: 'num qs-note', text: n(o.ideal) })));
|
||
const dist = (m.distribution ?? []).slice(0, 16).map((d) => el('tr', {},
|
||
el('th', { scope: 'row', text: d.bits }), el('td', { class: 'num', text: n(d.raw, 4) }), el('td', { class: 'num', text: `${n(d.mitigated, 4)} ± ${n(d.stderr, 4)}` }), el('td', {})));
|
||
return el('div', { class: 'qs-block', id: 'qs-mitigation' }, el('div', { class: 'qs-k', text: `Mitigated: ${m.method}` }),
|
||
el('table', { class: 'qs-table' },
|
||
el('thead', {}, el('tr', {}, el('th', { text: '' }), el('th', { class: 'num', text: 'raw' }), el('th', { class: 'num', text: 'mitigated' }), el('th', { class: 'num', text: rows.length ? 'ideal (simulation)' : '' }))),
|
||
el('tbody', {}, ...rows, ...dist)),
|
||
el('p', { class: 'qs-note', id: 'qs-mitigation-bias', text: `Bias: ${m.assumptions?.bias ?? ''}` }),
|
||
el('p', { class: 'qs-note', text: `Error bars: ${m.assumptions?.variance ?? ''} Cost: ${m.cost?.circuits ?? 0} circuits, ${m.cost?.shots ?? 0} shots.` }));
|
||
}
|
||
const DRAFT = 'wai.quantum-studio.draft';
|
||
const PREDICT_FIRST = 'wai.quantum-studio.predict-first';
|
||
const ms = (t) => (t < 1 ? tr('frame.under_ms') : t < 10000 ? `${Math.round(t)} ms` : `${(t / 1000).toFixed(1)} s`);
|
||
const split = (s) => s.split(/[,\n]/).map((x) => x.trim()).filter(Boolean);
|
||
// Where a job ran, in the reader's words (the history keeps the engine's).
|
||
const WHERE = { 'this browser': 'frame.where_browser', 'the service': 'frame.where_service', 'this computer': 'frame.where_computer' };
|
||
const whereWord = (w) => {
|
||
if (WHERE[w]) return tr(WHERE[w]);
|
||
if (w === 'your hardware') return tr('frame.where_hardware');
|
||
const m = /^your provider (.+)$/.exec(w || '');
|
||
return m ? tr('frame.where_provider', { name: m[1] }) : w;
|
||
};
|
||
|
||
// Whose hand a line is in, in the editor's gutter.
|
||
class HandMark extends GutterMarker {
|
||
constructor(hand, title) { super(); this.hand = hand; this.title = title; }
|
||
eq(o) { return o.hand === this.hand && o.title === this.title; }
|
||
toDOM() { const d = document.createElement('span'); d.className = `qs-hand qs-hand-${this.hand}`; d.title = this.title; return d; }
|
||
}
|
||
const setHands = StateEffect.define();
|
||
const handsField = StateField.define({
|
||
create: () => RangeSet.empty,
|
||
update(set, tr) {
|
||
set = set.map(tr.changes);
|
||
for (const e of tr.effects) if (e.is(setHands)) set = e.value;
|
||
return set;
|
||
},
|
||
});
|
||
const handGutter = gutter({ class: 'qs-hand-gutter', markers: (v) => v.state.field(handsField), initialSpacer: () => new HandMark('spacer', '') });
|
||
|
||
// Bytes as standard base64, for a binary program sent to the engine.
|
||
function toBase64(bytes) {
|
||
let s = '';
|
||
for (let i = 0; i < bytes.length; i += 0x8000) s += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
|
||
return btoa(s);
|
||
}
|
||
|
||
function mount(root) {
|
||
const desktop = onDesktop();
|
||
// A host page with a heading of its own mounts the Studio with data-embedded, and its name is not a second h1.
|
||
const embedded = 'embedded' in root.dataset;
|
||
const native = desktop ? new DesktopEngine() : new TabEngine(root.dataset.enginePkg, root.dataset.worker || new URL('worker.js', import.meta.url).href);
|
||
// A generator (a program with integer inputs) is read at the inputs bar's
|
||
// values: the engine here writes it out, for a run here or on the service.
|
||
const inputsChanged = StateEffect.define();
|
||
const inputs = inputsBar(() => { view.dispatch({ effects: inputsChanged.of(null) }); forceLinting(view); edited(); });
|
||
const local = withInputs(native, inputs.values);
|
||
const service = root.dataset.service ? withInputs(new ServiceEngine(root.dataset.service), inputs.values, native) : null;
|
||
// The person's own providers: in the desktop app only, which holds their credentials.
|
||
const hardware = desktop ? withInputs(new HardwareEngine(window.__TAURI__.core.invoke, askDialog(root)), inputs.values, native) : null;
|
||
let where = 'local';
|
||
let current = null; // {job, where, took}
|
||
let running = null;
|
||
let serviceKey = null;
|
||
let mySigner = null;
|
||
|
||
// ---- the frame ----
|
||
const status = el('span', { class: 'qs-status', role: 'status', 'aria-live': 'polite' });
|
||
const runBtn = el('button', { class: 'qs-btn qs-primary', id: 'qs-run', type: 'button', title: tr('frame.run_title'), text: tr('frame.run') });
|
||
const cancelBtn = el('button', { class: 'qs-btn', id: 'qs-cancel', type: 'button', hidden: true, text: tr('frame.cancel') });
|
||
// A run on a provider can leave its job there, still queued, and free the Studio.
|
||
const laterBtn = el('button', { class: 'qs-btn', id: 'qs-later', type: 'button', hidden: true, text: tr('hardware.later'), title: tr('hardware.later_title') });
|
||
const predictFirst = el('input', { type: 'checkbox', id: 'qs-predict-first' });
|
||
try { predictFirst.checked = localStorage.getItem(PREDICT_FIRST) === '1'; } catch (e) {}
|
||
predictFirst.addEventListener('change', () => { try { localStorage.setItem(PREDICT_FIRST, predictFirst.checked ? '1' : '0'); } catch (e) {} });
|
||
const predictLabel = el('label', { class: 'qs-check', for: 'qs-predict-first', title: tr('frame.predict_title') }, predictFirst, el('span', { text: tr('frame.predict') }));
|
||
const whereLocal = el('button', { class: 'qs-seg on', type: 'button', 'aria-pressed': 'true', 'data-where': 'local', text: desktop ? tr('frame.this_computer') : tr('frame.this_browser') });
|
||
const whereService = el('button', { class: 'qs-seg', type: 'button', 'aria-pressed': 'false', 'data-where': 'service', text: tr('frame.the_service'), disabled: !service });
|
||
const hw = hardwareSegment({ desktop, engine: hardware, onPick: () => choose(hw.seg), say: (m) => setStatus(m), jobs: () => jobsDialog(root, { engine: hardware, show: collected, say: (m) => setStatus(m) }) });
|
||
const examples = el('select', { class: 'qs-select', id: 'qs-example', 'aria-label': tr('frame.examples_label') },
|
||
el('option', { value: '', text: tr('frame.examples') }), ...EXAMPLES.map((e) => el('option', { value: e.id, text: e.title })));
|
||
// ---- the course: lessons with exercises the engine checks ----
|
||
const course = coursePane({
|
||
engine: native,
|
||
program: () => text(),
|
||
load: async (qasm, l) => {
|
||
if (!(await from(`the lesson ${l.id}`, () => setText(qasm)))) return false;
|
||
shows.textContent = tr('frame.lesson_loaded', { title: l.title });
|
||
return true;
|
||
},
|
||
ask: {
|
||
available: () => aiConnected,
|
||
hint: (prompt) => new Promise((resolve, reject) => {
|
||
assistantFor().ask(prompt, (ev) => { if (ev.type === 'answer') resolve({ text: ev.text, proposals: ev.proposals || [] }); }).then(() => resolve({ text: '', proposals: [] }), reject);
|
||
}),
|
||
},
|
||
status: (m, cls) => setStatus(m, cls),
|
||
});
|
||
// ---- the language, followed from the browser until the reader picks one ----
|
||
const langSwitch = el('select', { class: 'qs-select', id: 'qs-lang', 'aria-label': tr('frame.language') },
|
||
...Object.keys(LOCALES).map((l) => el('option', { value: l, text: NAMES[l] || l, selected: l === locale() })));
|
||
langSwitch.addEventListener('change', () => chooseAndReload(langSwitch.value));
|
||
const header = el('header', { class: 'qs-top' },
|
||
el('div', { class: 'qs-brand' }, el('span', { class: 'qs-mark', 'aria-hidden': 'true', text: '⟨ψ⟩' }), el(embedded ? 'span' : 'h1', { class: 'qs-title', text: tr('frame.studio') })),
|
||
examples, course.button,
|
||
el('div', { class: 'qs-where', role: 'group', 'aria-label': tr('frame.run_on') }, whereLocal, whereService, hw.seg), hw.picker,
|
||
predictLabel, runBtn, cancelBtn, laterBtn, langSwitch);
|
||
|
||
// ---- the reader's rules for proposals ----
|
||
const genInputs = () => (declared(text()).length ? inputs.values() : {});
|
||
const rules = rulesPane({
|
||
selection: () => {
|
||
const st = view.state, sel = st.selection.main;
|
||
const a = st.doc.lineAt(sel.from).number;
|
||
let z = st.doc.lineAt(sel.to).number;
|
||
if (z > a && sel.to === st.doc.line(z).from) z -= 1; // a selection that ends at a line's start stops before it
|
||
return [a, z];
|
||
},
|
||
hold: (a, z) => {
|
||
const doc = view.state.doc;
|
||
view.dispatch({ changes: [{ from: doc.line(a).from, insert: `${LOCK}\n` }, { from: doc.line(z).to, insert: `\n${UNLOCK}` }] });
|
||
setStatus(tr('frame.held', { a, z }), 'ok');
|
||
},
|
||
changed: () => paintHeld(),
|
||
});
|
||
// The Studio's features (flags.js), held by a study the reader takes part in.
|
||
const flags = featureFlags(root);
|
||
const studyRec = studyLog();
|
||
if (studyRec.enrolled()) flags.lock(studyRec.state().flags || {});
|
||
const ruleSet = () => (flags.on('rules') ? rules.locks() : {});
|
||
const brief = async () => (await local.locks('brief', { base: text(), locks: ruleSet() })).rules;
|
||
|
||
// ---- the program ----
|
||
const lintBadge = el('span', { class: 'qs-badge', id: 'qs-lint', text: '…' });
|
||
const fileIn = el('input', { type: 'file', id: 'qs-open-file', accept: '.qasm,.ll,.qir,.jeff,.txt,text/plain', hidden: true, 'aria-hidden': 'true' });
|
||
const shows = el('p', { class: 'qs-shows', id: 'qs-shows' });
|
||
const reviewBox = el('div', { class: 'qs-review', id: 'qs-review', hidden: true, role: 'region', 'aria-label': tr('frame.review_label') });
|
||
const editorHost = el('div', { class: 'qs-editor', id: 'qs-editor' });
|
||
const shareBtn = el('button', { class: 'qs-btn qs-small', type: 'button', text: tr('frame.copy_link'), title: tr('frame.copy_link_title') });
|
||
const openBtn = el('button', { class: 'qs-btn qs-small', type: 'button', text: tr('frame.open') });
|
||
const saveBtn = el('button', { class: 'qs-btn qs-small', type: 'button', text: tr('frame.save') });
|
||
|
||
const field = (id, label, attrs = {}) => {
|
||
const input = el(attrs.tag || 'input', { class: 'qs-input', id, ...attrs, tag: null });
|
||
return [input, el('label', { class: 'qs-field', for: id }, el('span', { text: label }), input)];
|
||
};
|
||
const [shots, shotsF] = field('qs-shots', tr('frame.shots'), { type: 'number', min: '0', step: '1', placeholder: '1024', inputmode: 'numeric' });
|
||
const [seed, seedF] = field('qs-seed', tr('frame.seed'), { type: 'number', min: '0', step: '1', placeholder: '1', inputmode: 'numeric' });
|
||
const method = el('select', { class: 'qs-input', id: 'qs-method' }, el('option', { value: 'auto', text: 'auto', title: tr('frame.auto_title') }));
|
||
const methodF = el('label', { class: 'qs-field', for: 'qs-method' }, el('span', { text: tr('frame.method') }), method);
|
||
const [obs, obsF] = field('qs-obs', tr('frame.observables'), { placeholder: 'Z0 Z1, 0.5 X2', autocomplete: 'off', spellcheck: 'false' });
|
||
const [amps, ampsF] = field('qs-amps', tr('frame.amplitudes'), { placeholder: '000, 111', autocomplete: 'off', spellcheck: 'false' });
|
||
const noiseInputs = NOISE.map(([k, label]) => field(`qs-noise-${k}`, label, { type: 'number', min: '0', max: '1', step: '0.001', placeholder: '0', 'data-noise': k }));
|
||
// Mitigation of the job's values under its noise (engine::mitigate).
|
||
const mitigate = el('select', { class: 'qs-input', id: 'qs-mitigate' }, ...MITIGATE.map(([v, t]) => el('option', { value: v, text: t })));
|
||
const mitigateF = el('label', { class: 'qs-field', for: 'qs-mitigate' }, el('span', { text: 'Mitigate' }), mitigate);
|
||
// A device target: its noise is its own, so the noise fields wait while one is chosen.
|
||
const noiseNote = el('p', { class: 'qs-note', id: 'qs-noise-note', hidden: true, text: tr('frame.device_noise') });
|
||
const device = devicePane({
|
||
engine: native,
|
||
changed: (t) => {
|
||
for (const [input] of noiseInputs) input.disabled = !!t;
|
||
noiseNote.hidden = !t;
|
||
plan();
|
||
},
|
||
say: (m, cls) => setStatus(m, cls),
|
||
});
|
||
const jobBox = el('div', { class: 'qs-job' },
|
||
el('div', { class: 'qs-row' }, shotsF, seedF, methodF),
|
||
el('div', { class: 'qs-row' }, obsF, ampsF),
|
||
el('details', { class: 'qs-noise' }, el('summary', { text: tr('frame.noise') }), noiseNote, el('div', { class: 'qs-row' }, ...noiseInputs.map((n) => n[1])), el('div', { class: 'qs-row' }, mitigateF)),
|
||
device.node);
|
||
const program = el('section', { class: 'qs-pane qs-program', 'aria-labelledby': 'qs-program-h' },
|
||
el('div', { class: 'qs-head' }, el('h2', { id: 'qs-program-h', text: tr('frame.program') }), lintBadge, el('span', { class: 'qs-grow' }), openBtn, saveBtn, shareBtn, fileIn),
|
||
shows, reviewBox, editorHost, inputs.node, rules.node, jobBox);
|
||
|
||
// ---- the circuit ----
|
||
const circuit = el('pre', { class: 'qs-circuit', id: 'qs-circuit', tabindex: '0', 'aria-label': tr('frame.circuit_text_label'), hidden: true });
|
||
// The engine's account of the circuit in words, which a screen reader reads for the diagram.
|
||
const described = el('p', { class: 'qs-sr', id: 'qs-describe' });
|
||
const diagram = el('div', { class: 'qs-diagram', id: 'qs-diagram', tabindex: '0', 'aria-describedby': 'qs-describe' });
|
||
const legend = el('p', { class: 'qs-note qs-legend', id: 'qs-legend' });
|
||
// The drawing kept as a file, as the engine writes it.
|
||
const picture = async (format, name, type) => {
|
||
try {
|
||
const v = await local.picture(await local.instance(text()), format);
|
||
if (desktop) { const r = await local.saveProgram(name, v[format]); setStatus(tr('frame.saved', { path: r.path }), 'ok'); }
|
||
else { download(name, v[format], type); setStatus(tr('frame.saved_drawing', { name }), 'ok'); }
|
||
} catch (e) { setStatus(e.message, 'bad'); }
|
||
};
|
||
const asSvg = el('button', { class: 'qs-btn qs-small', id: 'qs-export-svg', type: 'button', text: 'SVG', title: tr('frame.svg_title'), onclick: () => picture('svg', 'circuit.svg', 'image/svg+xml') });
|
||
const asTex = el('button', { class: 'qs-btn qs-small', id: 'qs-export-tex', type: 'button', text: 'LaTeX', title: tr('frame.latex_title'), onclick: () => picture('latex', 'circuit.tex', 'text/x-tex') });
|
||
// Editing by the drawing: each edit made in the program's text by the engine, and recorded as the reader's.
|
||
const drawEdits = circuitEditor({
|
||
engine: local,
|
||
program: () => text(),
|
||
apply: (v) => {
|
||
if (versions.busy()) { setStatus(tr('frame.choose_first'), 'bad'); return false; }
|
||
view.dispatch({ changes: { from: v.edit.from_utf16, to: v.edit.to_utf16, insert: v.edit.insert } });
|
||
return true;
|
||
},
|
||
status: (m, cls) => setStatus(m, cls),
|
||
});
|
||
drawEdits.bind(diagram);
|
||
const asText = el('button', { class: 'qs-btn qs-small', id: 'qs-astext', type: 'button', 'aria-pressed': 'false', text: tr('frame.text'), title: tr('frame.text_title') });
|
||
asText.addEventListener('click', () => {
|
||
const on = circuit.hidden;
|
||
circuit.hidden = !on; diagram.hidden = on;
|
||
asText.setAttribute('aria-pressed', String(on)); asText.textContent = on ? tr('frame.diagram') : tr('frame.text');
|
||
});
|
||
// The critique, kept up as the program changes, and marked on the diagram.
|
||
// A smell's gates are marked; a smell about whole qubits (idle, unmeasured) marks their wires.
|
||
const wires = (s) => (s?.ops?.length ? [] : s?.qubits);
|
||
const critique = critiquePanel({
|
||
onShow: (s) => {
|
||
mark(diagram, 'qc-smell', s.ops, wires(s));
|
||
diagram.querySelector('.qc-smell')?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
||
if (!s.lines?.length) return;
|
||
const doc = view.state.doc;
|
||
const a = doc.line(Math.min(Math.min(...s.lines), doc.lines)), b = doc.line(Math.min(Math.max(...s.lines), doc.lines));
|
||
view.dispatch({ selection: { anchor: a.from, head: b.to }, scrollIntoView: true });
|
||
view.focus();
|
||
},
|
||
onFix: (s) => fix(s),
|
||
onMark: (s) => mark(diagram, 'qc-smell', s?.ops, wires(s)),
|
||
});
|
||
// The classical-shortcut lens, asked for: every exact route and its cost.
|
||
const lens = shortcutsPanel({ engine: local, program: () => text() });
|
||
// The rewrite lens, asked for: each rewrite that keeps the meaning, taken one at a time.
|
||
const rewrites = rewritePanel({ engine: local, program: () => text(), onMark: (ops) => mark(diagram, 'qc-rewrite', ops), offer: (qasm, note) => offer(qasm, { kind: 'engine', name: 'Rewrite lens' }, note) });
|
||
// The compile lens, asked for: the passes, each checked, and the circuit after any of them.
|
||
const compiler = compilePanel({ engine: local, program: () => text(), offer: (qasm, note) => offer(qasm, { kind: 'engine', name: 'Compile' }, note) });
|
||
// The estimate pane, asked for: the program's counts, what a fault-tolerant machine needs, the frontier.
|
||
const estimator = estimatePanel({ engine: local, program: () => text() });
|
||
const circuitPane = el('section', { class: 'qs-pane qs-circ', 'aria-labelledby': 'qs-circuit-h' },
|
||
el('div', { class: 'qs-head' }, el('h2', { id: 'qs-circuit-h', text: tr('frame.circuit') }), el('span', { class: 'qs-grow' }), el('span', { class: 'qs-note', id: 'qs-plan' }), asText, asSvg, asTex),
|
||
drawEdits.node, diagram, described, circuit, legend, critique.node, lens.node, rewrites.node, compiler.node, estimator.node);
|
||
// Step through: the engine's debugger, the next gate marked on the diagram.
|
||
const debug = debuggerPane({
|
||
engine: local,
|
||
program: () => text(),
|
||
job: () => request(),
|
||
inputs: () => inputs.values(),
|
||
onStep: (v) => mark(diagram, 'qc-step', v.next?.op != null ? [v.next.op] : []),
|
||
});
|
||
|
||
// ---- a while on one's own: the assistant only asks ----
|
||
const organic = organicMode();
|
||
|
||
// ---- the reader's taste, learned from their choices ----
|
||
const taste = tasteStore(native);
|
||
|
||
// ---- what was found worth a second look ----
|
||
const found = foundPane({ open: (it) => offer(it.source, it.by, `${it.title}, from Found.`) });
|
||
|
||
// ---- the alternatives ----
|
||
const gallery = galleryPane({
|
||
engine: local,
|
||
draft: () => text(),
|
||
model: () => (aiConnected ? assistantFor() : null),
|
||
apply: (qasm, note, by) => offer(qasm, by, note),
|
||
check: (base, qasm) => local.locks('check', { base, proposed: qasm, locks: ruleSet(), inputs: genInputs() }),
|
||
found,
|
||
taste,
|
||
paused: () => organic.on(),
|
||
features: flags,
|
||
onChoice: (c) => studyRec.choice(c),
|
||
});
|
||
|
||
// ---- the workbench ----
|
||
const workbench = workbenchPane({
|
||
engine: native,
|
||
draft: () => text(),
|
||
model: () => (aiConnected ? assistantFor() : null),
|
||
apply: (qasm, note, by) => offer(qasm, by, note),
|
||
save: (name, body) => saveFile(name, body, 'application/json'),
|
||
found,
|
||
taste,
|
||
paused: () => organic.on(),
|
||
features: flags,
|
||
onChoice: (c) => studyRec.choice(c),
|
||
});
|
||
|
||
// ---- the result ----
|
||
const state = el('span', { class: 'qs-badge', id: 'qs-state', text: tr('frame.ready') });
|
||
const resultBody = el('div', { class: 'qs-result', id: 'qs-result' }, el('p', { class: 'qs-empty', text: tr('frame.result_empty') }));
|
||
const resultPane = el('section', { class: 'qs-pane qs-res', 'aria-labelledby': 'qs-result-h' },
|
||
el('div', { class: 'qs-head' }, el('h2', { id: 'qs-result-h', text: tr('frame.result') }), state), resultBody);
|
||
|
||
// ---- the history ----
|
||
const historyList = el('ol', { class: 'qs-history', id: 'qs-history' });
|
||
const historyPane = el('section', { class: 'qs-pane qs-hist', 'aria-labelledby': 'qs-history-h' },
|
||
el('div', { class: 'qs-head' }, el('h2', { id: 'qs-history-h', text: tr('frame.history') }), el('span', { class: 'qs-grow' }),
|
||
el('button', { class: 'qs-btn qs-small', type: 'button', text: tr('frame.clear'), onclick: async () => { await History.clear(); showHistory(); } })),
|
||
historyList);
|
||
|
||
// ---- the assistant ----
|
||
const AI_ITEM = 'wai.quantum-studio.assistant';
|
||
const aiConf = { endpoint: 'http://127.0.0.1:8080/v1', model: '', stance: 'questions', initiative: 'asked' };
|
||
try { Object.assign(aiConf, JSON.parse(localStorage.getItem(AI_ITEM) || '{}')); } catch (e) {}
|
||
const [aiEndpoint, aiEndpointF] = field('qs-ai-endpoint', tr('ai.server'), { value: aiConf.endpoint, spellcheck: 'false', autocomplete: 'off' });
|
||
const [aiModel, aiModelF] = field('qs-ai-model', tr('ai.model'), { value: aiConf.model, placeholder: tr('ai.model_placeholder'), spellcheck: 'false', autocomplete: 'off' });
|
||
const aiState = el('span', { class: 'qs-badge', id: 'qs-ai-state', text: tr('ai.not_connected') });
|
||
const aiConnect = el('button', { class: 'qs-btn qs-small', id: 'qs-ai-connect', type: 'button', text: tr('ai.connect') });
|
||
const aiStance = el('select', { class: 'qs-select', id: 'qs-ai-stance' }, ...Object.entries(STANCES).map(([k, v]) => el('option', { value: k, text: v.label })));
|
||
aiStance.value = STANCES[aiConf.stance] ? aiConf.stance : 'questions';
|
||
const aiInitiative = el('select', { class: 'qs-select', id: 'qs-ai-initiative' },
|
||
el('option', { value: 'asked', text: tr('ai.when_asked') }), el('option', { value: 'hint', text: tr('ai.when_stuck') }));
|
||
aiInitiative.value = aiConf.initiative === 'hint' ? 'hint' : 'asked';
|
||
const keepConf = () => { try { localStorage.setItem(AI_ITEM, JSON.stringify({ endpoint: aiEndpoint.value.trim(), model: aiModel.value.trim(), stance: aiStance.value, initiative: aiInitiative.value })); } catch (e) {} };
|
||
aiStance.addEventListener('change', keepConf);
|
||
aiInitiative.addEventListener('change', () => { keepConf(); if (aiInitiative.value === 'asked') stuck.hidden = true; });
|
||
const aiModes = el('div', { class: 'qs-ai-modes' },
|
||
el('label', { class: 'qs-field', for: 'qs-ai-stance' }, el('span', { text: tr('ai.how') }), aiStance),
|
||
el('label', { class: 'qs-field', for: 'qs-ai-initiative' }, el('span', { text: tr('ai.when') }), aiInitiative));
|
||
const aiSettings = el('div', { class: 'qs-ai-settings', id: 'qs-ai-settings', hidden: true },
|
||
el('div', { class: 'qs-row' }, aiEndpointF, aiModelF),
|
||
el('div', { class: 'qs-actions' }, aiConnect),
|
||
el('p', { class: 'qs-note', text: tr('ai.settings_note') }));
|
||
const aiLog = el('ol', { class: 'qs-ai-log', id: 'qs-ai-log', 'aria-live': 'polite' },
|
||
el('li', { class: 'qs-note', text: tr('ai.intro') }));
|
||
const aiInput = el('textarea', { class: 'qs-input qs-ai-input', id: 'qs-ai-input', rows: '2', placeholder: tr('ai.placeholder'), 'aria-label': tr('ai.ask_label') });
|
||
const aiSend = el('button', { class: 'qs-btn qs-primary', id: 'qs-ai-send', type: 'button', text: tr('ai.ask') });
|
||
// Offered, never sent: with “Offer a hint when I seem stuck”, a problem the
|
||
// engine has reported for a while, or runs refused twice in a row, show this.
|
||
const stuck = el('button', { class: 'qs-btn qs-small qs-stuck', id: 'qs-ai-stuck', type: 'button', hidden: true, text: tr('ai.stuck'), onclick: () => { stuck.hidden = true; ask(HINT, tr('ai.q.hint')); } });
|
||
const STUCK_MS = Number(root.dataset.stuckMs) || 45000;
|
||
let badSince = null, stuckTimer = null, refusals = 0;
|
||
const offerHint = () => { if (aiInitiative.value === 'hint') stuck.hidden = false; };
|
||
const HINT = 'Give me one hint toward what is wrong or could be better in this program: point at where to look, not at the fix. Use the tools to find it first.';
|
||
const QUICK = [
|
||
[tr('ai.q.question'), 'Ask me one question about this program that would help me see what it does or how to make it better. Use the tools first if they help, and do not give the answer.'],
|
||
[tr('ai.q.hint'), HINT],
|
||
[tr('ai.q.explain'), 'Explain what this program does, step by step, and what its result shows.'],
|
||
[tr('ai.q.method'), 'Why does the engine pick this method for this job, and what would make it pick another?'],
|
||
[tr('ai.q.fix'), 'The program has a problem. Find it with the tools and give the corrected whole program.'],
|
||
];
|
||
// A correctness record of the model's answers, on this device, off until the reader turns it on.
|
||
const telemetry = telemetryStore({ engine: local });
|
||
const aiPane = el('section', { class: 'qs-pane qs-ai', 'aria-labelledby': 'qs-ai-h' },
|
||
el('div', { class: 'qs-head' }, el('h2', { id: 'qs-ai-h', text: tr('ai.title') }), aiState, el('span', { class: 'qs-grow' }),
|
||
el('button', { class: 'qs-btn qs-small', type: 'button', id: 'qs-ai-gear', text: tr('ai.gear'), 'aria-controls': 'qs-ai-settings', onclick: () => { aiSettings.hidden = !aiSettings.hidden; } })),
|
||
aiSettings, aiModes, telemetryPanel({ store: telemetry, save: (name, body, type) => saveFile(name, body, type) }),
|
||
el('div', { class: 'qs-ai-quick' }, ...QUICK.map(([label, q]) => el('button', { class: 'qs-btn qs-small', type: 'button', text: label, onclick: () => ask(q, label) })), stuck),
|
||
aiLog,
|
||
el('div', { class: 'qs-ai-ask' }, aiInput, aiSend));
|
||
|
||
// ---- answers in the answer view layer, drawn as they arrive ----
|
||
const avInput = el('textarea', { class: 'qs-input qs-av-input', id: 'qs-av-input', rows: '6', spellcheck: 'false', placeholder: 'root = Answer([bell, counts])\n```qasm bell\n…\n```\ncounts = Histogram(@Sample(bell, 1000))', 'aria-label': 'An answer in the answer view layer' });
|
||
const avQ = el('input', { class: 'qs-input', id: 'qs-av-q', placeholder: 'Ask the model for an answer about this program…', 'aria-label': 'Ask the model for an answer' });
|
||
const avDraw = el('button', { class: 'qs-btn qs-small', type: 'button', id: 'qs-av-draw', text: 'Draw' });
|
||
const avAsk = el('button', { class: 'qs-btn qs-small', type: 'button', id: 'qs-av-ask', text: 'Ask the model' });
|
||
const avAfter = el('div', { class: 'qs-actions', id: 'qs-av-after', hidden: true });
|
||
const avStatus = el('p', { class: 'qs-note', id: 'qs-av-status' });
|
||
const answers = answerRenderer({ engine: local, onEnded: (done) => answerEnded(done) });
|
||
const answerPane = el('details', { class: 'qs-pane qs-answers', id: 'qs-answers' }, el('summary', { text: 'Answer views' }),
|
||
el('p', { class: 'qs-note', text: 'An answer in the answer view layer: the program, and views whose numbers come from engine calls written with @. Each view is drawn as its statement arrives, its call carried out by the engine here; a number the model wrote in a view is refused with its code.' }),
|
||
el('div', { class: 'qs-row' }, avQ, avAsk), avInput, el('div', { class: 'qs-actions' }, avDraw), avStatus, answers.node, avAfter);
|
||
|
||
const footer = el('footer', { class: 'qs-foot' }, el('span', { class: 'qs-signer', id: 'qs-signer' }), status,
|
||
machineAssisted() ? el('span', { class: 'qs-note', id: 'qs-translation-note', text: tr('frame.machine_assisted') }) : null);
|
||
root.classList.add('qs');
|
||
// ---- the versions ----
|
||
const editable = new Compartment();
|
||
const handsCol = new Compartment();
|
||
let converge = null;
|
||
const versions = versionsPane({
|
||
engine: local,
|
||
text: () => text(),
|
||
setText: (s) => setText(s),
|
||
inputs: genInputs,
|
||
rules: () => ruleSet(),
|
||
taken: () => { if (rules.region()) rules.setRegion(null); },
|
||
lock: (on) => { view.dispatch({ effects: editable.reconfigure(EditorState.readOnly.of(on)) }); editorHost.classList.toggle('qs-locked', on); },
|
||
reviewHost: reviewBox,
|
||
onHands: (b) => showHands(b),
|
||
save: (name, body, type) => saveFile(name, body, type),
|
||
status: (s, cls) => setStatus(s, cls),
|
||
editNote: () => (organic.on() ? ORGANIC : null),
|
||
onRender: () => converge?.show(),
|
||
});
|
||
// Whether the reader's versions converge on the model's, under Versions.
|
||
converge = convergePanel({ engine: native, history: () => versions.history(), inputs: genInputs, organic });
|
||
versions.node.append(converge.node);
|
||
|
||
// ---- a study the reader takes part in, and their own record ----
|
||
const studyUi = studyPane({ engine: native, log: studyRec, flags, history: () => versions.history(), inputs: genInputs, signer: () => local.signer(), save: (name, body, type) => saveFile(name, body, type), organic, featuresView: flagsView(flags) });
|
||
// The features a study sets reach the controls they govern.
|
||
const settle = () => {
|
||
let own = false;
|
||
try { own = localStorage.getItem(PREDICT_FIRST) === '1'; } catch (e) {}
|
||
predictFirst.checked = flags.held('predict_first') ? flags.on('predict_first') : flags.on('predict_first') && own;
|
||
predictFirst.disabled = flags.held('predict_first');
|
||
aiStance.disabled = flags.held('assistant_asks') || !flags.on('assistant_asks');
|
||
};
|
||
flags.onChange(settle);
|
||
flags.apply();
|
||
|
||
// ---- error correction: a code, an error placed on it, its decode, a threshold ----
|
||
const qec = qecPane({ engine: local });
|
||
// ---- pulse: the program's schedule on frames, and a run of it under the master equation ----
|
||
const pulse = pulsePane({ engine: local, source: () => text(), load: (s) => setText(s) });
|
||
|
||
root.replaceChildren(header, course.node, el('div', { class: 'qs-grid' }, program, el('div', { class: 'qs-side' }, circuitPane, debug.node, gallery.node, workbench.node, found.node, resultPane, qec.node, pulse.node, versions.node, aiPane, answerPane, historyPane, studyUi.node)), footer);
|
||
// On the desktop, a newer version found as the app opens, offered above the frame.
|
||
if (desktop) {
|
||
const update = updateBar({ updates: new Updates(window.__TAURI__.core.invoke), say: (m, cls) => setStatus(m, cls) });
|
||
root.prepend(update.node);
|
||
setTimeout(() => update.check(), 3000);
|
||
}
|
||
|
||
// ---- the editor ----
|
||
const style = HighlightStyle.define([
|
||
{ tag: t.keyword, color: 'var(--qs-kw)' },
|
||
{ tag: t.typeName, color: 'var(--qs-type)' },
|
||
{ tag: t.function(t.variableName), color: 'var(--qs-gate)', fontWeight: '600' },
|
||
{ tag: t.number, color: 'var(--qs-num)' },
|
||
{ tag: t.atom, color: 'var(--qs-num)' },
|
||
{ tag: t.standard(t.variableName), color: 'var(--qs-type)' },
|
||
{ tag: t.string, color: 'var(--qs-str)' },
|
||
{ tag: t.comment, color: 'var(--text-muted)', fontStyle: 'italic' },
|
||
{ tag: t.operator, color: 'var(--text-secondary)' },
|
||
]);
|
||
const lintAt = async (view) => {
|
||
const text = view.state.doc.toString();
|
||
let v;
|
||
try { v = await local.lint(text); } catch (e) { lintBadge.textContent = tr('frame.engine_not_loaded'); lintBadge.className = 'qs-badge bad'; return []; }
|
||
if (v.ok) {
|
||
const at = declared(text).length ? tr('frame.lint_at', { values: Object.entries(inputs.values()).map(([n, x]) => `${n} = ${x}`).join(', ') }) : '';
|
||
lintBadge.textContent = `${tr('frame.lint', { qubits: v.qubits, gates: v.gates })}${at}${v.dynamic_steps != null ? tr('frame.lint_dynamic') : ''}`;
|
||
lintBadge.className = 'qs-badge ok';
|
||
badSince = null; clearTimeout(stuckTimer);
|
||
if (!refusals) stuck.hidden = true;
|
||
return [];
|
||
}
|
||
lintBadge.textContent = tr('frame.problem'); lintBadge.className = 'qs-badge bad';
|
||
if (badSince == null) { badSince = Date.now(); clearTimeout(stuckTimer); stuckTimer = setTimeout(() => { if (badSince != null) offerHint(); }, STUCK_MS); }
|
||
const doc = view.state.doc;
|
||
const line = v.line && v.line <= doc.lines ? doc.line(v.line) : doc.line(doc.lines);
|
||
return [{ from: line.from, to: Math.max(line.to, line.from), severity: 'error', message: v.error }];
|
||
};
|
||
const theme = EditorView.theme({
|
||
'&': { height: '100%', fontSize: '13px', backgroundColor: 'var(--bg-base)', color: 'var(--text-primary)' },
|
||
'.cm-content': { fontFamily: 'var(--font-mono)', caretColor: 'var(--accent)' },
|
||
'.cm-gutters': { backgroundColor: 'var(--bg-surface)', color: 'var(--text-muted)', borderRight: '1px solid var(--border)' },
|
||
'.cm-activeLine': { backgroundColor: 'color-mix(in srgb, var(--accent) 6%, transparent)' },
|
||
'.cm-activeLineGutter': { backgroundColor: 'color-mix(in srgb, var(--accent) 10%, transparent)' },
|
||
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground': { backgroundColor: 'color-mix(in srgb, var(--accent) 25%, transparent) !important' },
|
||
'.cm-tooltip': { backgroundColor: 'var(--bg-elevated)', border: '1px solid var(--border-subtle)', color: 'var(--text-primary)' },
|
||
});
|
||
const view = new EditorView({
|
||
parent: editorHost,
|
||
state: EditorState.create({
|
||
doc: '',
|
||
extensions: [
|
||
handsField, handsCol.of([]), editable.of(EditorState.readOnly.of(false)), heldLines,
|
||
lineNumbers(), highlightActiveLineGutter(), highlightActiveLine(), history(), drawSelection(), indentOnInput(), bracketMatching(), closeBrackets(),
|
||
highlightSelectionMatches(), openqasm, syntaxHighlighting(style), autocompletion({ override: [complete] }),
|
||
// A new value for a generator's input re-reads the program, as an edit does.
|
||
linter(lintAt, { delay: 300, needsRefresh: (u) => u.transactions.some((tr) => tr.effects.some((e) => e.is(inputsChanged))) }), lintGutter(), theme,
|
||
EditorView.contentAttributes.of({ 'aria-label': tr('frame.editor_label') }),
|
||
keymap.of([{ key: 'Mod-Enter', run: () => { run(); return true; } }, { key: 'Escape', run: () => { if (running) { cancel(); return true; } return false; } },
|
||
...closeBracketsKeymap, ...defaultKeymap, ...searchKeymap, ...historyKeymap, ...completionKeymap, indentWithTab]),
|
||
EditorView.updateListener.of((u) => { if (u.docChanged) { edited(); versions.edited(); } }),
|
||
],
|
||
}),
|
||
});
|
||
const text = () => view.state.doc.toString();
|
||
const setText = (s) => view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: s } });
|
||
|
||
// ---- the job ----
|
||
function request() {
|
||
const job = { qasm: text() };
|
||
const n = parseInt(shots.value, 10); if (Number.isFinite(n) && n > 0) job.shots = n;
|
||
const s = parseInt(seed.value, 10); if (Number.isFinite(s) && s >= 0 && seed.value !== '') job.seed = s;
|
||
const o = split(obs.value); if (o.length) job.observables = o;
|
||
const a = split(amps.value); if (a.length) job.amplitudes = a;
|
||
const noise = {};
|
||
for (const [input] of noiseInputs) { const v = parseFloat(input.value); if (Number.isFinite(v) && v > 0) noise[input.dataset.noise] = v; }
|
||
const target = device.target();
|
||
if (target) job.target = target;
|
||
else if (Object.keys(noise).length) job.noise = noise;
|
||
if (method.value !== 'auto') job.method = method.value;
|
||
if (mitigate.value !== 'none' && job.noise) job.mitigate = mitigate.value;
|
||
return job;
|
||
}
|
||
function fill(job) {
|
||
setText(job.qasm || '');
|
||
shots.value = job.shots ?? ''; seed.value = job.seed ?? '';
|
||
obs.value = (job.observables || []).join(', '); amps.value = (job.amplitudes || []).join(', ');
|
||
for (const [input] of noiseInputs) input.value = job.noise?.[input.dataset.noise] ?? '';
|
||
method.value = [...method.options].some((o) => o.value === job.method) ? job.method : 'auto';
|
||
const m = typeof job.mitigate === 'string' ? job.mitigate : job.mitigate?.method;
|
||
mitigate.value = [...mitigate.options].some((o) => o.value === m) ? m : 'none';
|
||
if (job.noise && Object.keys(job.noise).length) jobBox.querySelector('details').open = true;
|
||
if (job.target) {
|
||
device.set(job.target).catch(() => {});
|
||
device.node.open = true;
|
||
}
|
||
}
|
||
|
||
// ---- drawing and planning, as the program changes ----
|
||
let drawTimer = null;
|
||
function edited() {
|
||
shows.textContent = '';
|
||
inputs.sync(text());
|
||
clearTimeout(drawTimer);
|
||
drawTimer = setTimeout(async () => {
|
||
const src = text();
|
||
try { localStorage.setItem(DRAFT, src); } catch (e) {}
|
||
lens.stale(src);
|
||
rewrites.stale(src);
|
||
compiler.stale(src);
|
||
estimator.stale(src);
|
||
try {
|
||
const [d, l, c, words] = await Promise.all([local.draw(src, Math.max(40, Math.floor((circuit.clientWidth || diagram.clientWidth) / 7.8) || 80)), local.layout(src), local.critique(src), local.instance(src).then((q) => local.picture(q, 'description')).catch(() => null)]);
|
||
circuit.textContent = d.text ?? '';
|
||
circuit.classList.remove('stale');
|
||
const { svg, drawn, total, geometry } = circuitSvg(l);
|
||
diagram.replaceChildren(svg);
|
||
diagram.classList.remove('stale');
|
||
drawEdits.attach(svg, geometry);
|
||
described.textContent = words?.description ?? '';
|
||
debug.stale();
|
||
legend.textContent = [...l.legend.map((c, i) => `${i + 1}: ${c}`), drawn < total ? `The first ${drawn} of ${total} columns are drawn; “Text” shows them all.` : ''].filter(Boolean).join(' · ');
|
||
critique.update(c);
|
||
paintHands();
|
||
paintHeld();
|
||
// The smells, on the diagram: each gate in one is flagged, its tooltip saying why.
|
||
const smells = c.ok ? c.smells : [];
|
||
if (flags.on('critique')) mark(diagram, 'qc-flag', smells.flatMap((s) => s.ops), smells.flatMap((s) => s.qubits && !s.ops.length ? s.qubits : []));
|
||
else mark(diagram, 'qc-flag', []);
|
||
for (const s of smells) for (const op of s.ops) annotate(diagram, op, s.message);
|
||
} catch (e) {
|
||
critique.update({ ok: false, error: e.message });
|
||
circuit.textContent = e.message; circuit.classList.add('stale');
|
||
diagram.replaceChildren(el('p', { class: 'qs-note', text: e.message })); diagram.classList.add('stale');
|
||
legend.textContent = '';
|
||
}
|
||
plan();
|
||
}, 250);
|
||
}
|
||
async function plan() {
|
||
const p = document.getElementById('qs-plan');
|
||
if (where !== 'local') { p.textContent = ''; return; }
|
||
try {
|
||
const pl = await local.plan(request());
|
||
p.textContent = tr('frame.plan', { method: METHOD_NAMES[pl.method] || pl.method, mib: (pl.memory_bytes / 2 ** 20).toFixed(0) });
|
||
p.title = pl.why;
|
||
// On a device target, the layout the plan chose is drawn on its map.
|
||
device.showPlacement(pl.target?.placement, pl.target?.noise);
|
||
} catch (e) { p.textContent = e.message.length > 90 ? e.message.slice(0, 90) + '…' : e.message; p.title = e.message; device.showPlacement(null); }
|
||
}
|
||
|
||
// ---- running ----
|
||
function setStatus(s, cls = '') { status.textContent = s; status.className = 'qs-status ' + cls; }
|
||
function busy(on) { runBtn.disabled = on; cancelBtn.hidden = !on; laterBtn.hidden = !on || running !== hardware; }
|
||
// With “Predict first”, the reader says what they expect before the run.
|
||
async function run() {
|
||
if (running) return;
|
||
if (!predictFirst.checked) return go(null);
|
||
// The program as it is now, not as the last redraw saw it.
|
||
const c = await local.critique(text()).catch(() => null);
|
||
resultBody.replaceChildren(Predict.form({ width: c?.ok ? c.bits : 0, onRun: go }));
|
||
state.textContent = tr('frame.your_prediction'); state.className = 'qs-badge run';
|
||
}
|
||
async function go(prediction) {
|
||
if (running) return;
|
||
const job = request();
|
||
if (job.target && where === 'service') job.target = device.forService();
|
||
// Every run states how far the engine checked it, sealed by its receipt.
|
||
job.verdict = true;
|
||
const eng = where === 'service' ? service : where === 'hardware' ? hardware : local;
|
||
running = eng; busy(true);
|
||
state.textContent = tr('frame.running'); state.className = 'qs-badge run';
|
||
setStatus(tr('frame.running_on', { where: whereWord(eng.where) }));
|
||
// The history's account of the program: the receipt binds whose hand each gate is in.
|
||
try { job.history = await versions.attest(); } catch (e) {}
|
||
const t0 = performance.now();
|
||
try {
|
||
const body = await eng.run(job, (j, ...more) => setStatus(eng.progressText ? eng.progressText(j, ...more) : tr('frame.on_service', { status: j.status })));
|
||
const took = performance.now() - t0;
|
||
current = { job: body, where: eng.where, took, prediction };
|
||
refusals = 0;
|
||
critique.ran();
|
||
await show(current);
|
||
setStatus(tr('frame.done', { where: whereWord(eng.where), took: ms(took) }), 'ok');
|
||
await History.save({ key: body.result?.receipt?.digest || String(Date.now()), saved: Date.now(), where: eng.where, title: firstLine(job.qasm), job: body });
|
||
showHistory();
|
||
} catch (e) {
|
||
// Left at the provider, still queued: not a refusal, and its result comes later.
|
||
if (e.later) { state.textContent = tr('hardware.queued_badge'); state.className = 'qs-badge run'; setStatus(e.message); return; }
|
||
state.textContent = e.message === 'cancelled' ? tr('frame.cancelled_badge') : tr('frame.refused'); state.className = 'qs-badge bad';
|
||
if (e.message !== 'cancelled' && ++refusals >= 2) offerHint();
|
||
setStatus(e.message === 'cancelled' ? tr('frame.cancelled') : e.message, 'bad');
|
||
// A refusal states its level too: what the engine read, and the code it refused with.
|
||
if (e.message !== 'cancelled') {
|
||
const v = await local.verdict({ request: job, error: e.message }).catch(() => null);
|
||
if (v) resultBody.replaceChildren(el('div', { class: 'qs-block' }, el('div', { class: 'qs-k', text: tr('frame.verdict') }), el('div', {}, verdictLine(v, { id: 'qs-verdict' }))));
|
||
}
|
||
} finally { running = null; busy(false); }
|
||
}
|
||
function cancel() { running?.cancel(); }
|
||
// A job on a provider, finished: its result kept in the history, and shown
|
||
// when the person asked for it.
|
||
const providerOf = (body) => (body.request?.provider ? `your provider ${body.request.provider}` : 'your hardware');
|
||
async function keep(body) {
|
||
await History.save({ key: body.result?.receipt?.digest || body.id || String(Date.now()), saved: Date.now(), where: providerOf(body), title: firstLine(body.request?.qasm || ''), job: body });
|
||
showHistory();
|
||
}
|
||
async function collected(body) {
|
||
if (!(await from(`the job ${body.id} on ${body.request?.provider}`, () => fill(body.request || {})))) return;
|
||
current = { job: body, where: providerOf(body), took: 0 };
|
||
await show(current);
|
||
setStatus(tr('hardware.collected', { id: body.id, provider: body.request?.provider }), 'ok');
|
||
await keep(body);
|
||
}
|
||
const firstLine = (q) => (q.split('\n').find((l) => /\b(h|x|cx|rx|ry|rz|ccx|measure)\b/.test(l)) || q.split('\n')[0] || '').trim().slice(0, 60);
|
||
|
||
// ---- a result ----
|
||
async function show({ job, where: ran, took, prediction }) {
|
||
const r = job.result || {};
|
||
state.textContent = job.status || 'done'; state.className = 'qs-badge ok';
|
||
const acc = r.accuracy || {};
|
||
const extra = accuracyExtras(r);
|
||
const blocks = [
|
||
prediction ? Predict.block(prediction, r) : null,
|
||
el('div', { class: 'qs-block' }, el('div', { class: 'qs-k', text: tr('frame.method') }),
|
||
el('div', {}, el('b', { class: 'qs-method', text: METHOD_NAMES[job.method] || job.method || '' }), el('span', { class: 'qs-why', text: ' ' + (job.why || '') }))),
|
||
el('div', { class: 'qs-block' }, el('div', { class: 'qs-k', text: tr('frame.accuracy') }),
|
||
el('div', {}, el('span', { class: 'qs-badge ok', text: acc.class || '?' }), el('span', { class: 'qs-why', text: ' ' + (ACCURACY[acc.class] || '') + (extra.length ? ' ' + extra.join(', ') + '.' : '') })),
|
||
r.warning ? el('p', { class: 'qs-warn', text: r.warning }) : null),
|
||
el('div', { class: 'qs-block', id: 'qs-verdict-block' }, el('div', { class: 'qs-k', text: tr('frame.verdict') }), el('div', { id: 'qs-verdict-line' }, r.verdict ? verdictLine(r.verdict, { sealed: true, id: 'qs-verdict' }) : el('span', { class: 'qs-note', text: tr('frame.working_out') }))),
|
||
];
|
||
if (r.counts) blocks.push(countsBlock(r));
|
||
if (r.expectations) blocks.push(expectationsBlock(r));
|
||
if (r.mitigation) blocks.push(mitigationBlock(r.mitigation));
|
||
if (r.target) {
|
||
blocks.push(deviceBlock(r.target, r.ideal));
|
||
device.showPlacement(r.target.placement, r.target.noise);
|
||
}
|
||
if (r.amplitudes) blocks.push(amplitudesBlock(r));
|
||
// The receipt, checked here.
|
||
const rc = r.receipt || {};
|
||
const account = el('span', { class: 'qs-badge', id: 'qs-account', text: tr('frame.checking') });
|
||
const check = el('span', { class: 'qs-badge', id: 'qs-verified', text: tr('frame.checking') });
|
||
const energy = energyText(r.energy || {});
|
||
const dl = el('button', { class: 'qs-btn qs-small', type: 'button', text: tr('frame.download_job'), onclick: () => saveJob(job) });
|
||
blocks.push(el('div', { class: 'qs-block' }, el('div', { class: 'qs-k', text: tr('frame.receipt') }),
|
||
el('table', { class: 'qs-table' }, el('tbody', {},
|
||
el('tr', {}, el('th', { scope: 'row', text: tr('frame.ran_on') }), el('td', { text: whereWord(ran) })),
|
||
el('tr', {}, el('th', { scope: 'row', text: tr('frame.signed_by') }), el('td', { class: 'mono', id: 'qs-signed-by', text: (rc.key || '').slice(0, 16) + '…' })),
|
||
el('tr', {}, el('th', { scope: 'row', text: tr('frame.check') }), el('td', {}, check)),
|
||
el('tr', {}, el('th', { scope: 'row', text: tr('frame.ran_in') }), el('td', { text: r.elapsed_ms != null ? ms(r.elapsed_ms) : tr('frame.timed_here', { took: ms(took) }) })),
|
||
el('tr', {}, el('th', { scope: 'row', text: tr('frame.energy') }), el('td', { text: energy })),
|
||
job.request?.history ? el('tr', {}, el('th', { scope: 'row', text: tr('frame.written_by') }), el('td', { id: 'qs-written-by', text: writtenBy(job.request.history) })) : null,
|
||
job.request?.history ? el('tr', {}, el('th', { scope: 'row', text: tr('frame.account') }), el('td', {}, account)) : null)),
|
||
el('p', { class: 'qs-note', text: tr('frame.receipt_note') }),
|
||
el('div', { class: 'qs-actions' }, dl)));
|
||
resultBody.replaceChildren(...blocks.filter(Boolean));
|
||
// A result that carries no verdict (from a host before verdicts) has one worked out here.
|
||
if (!r.verdict) {
|
||
const v = await local.verdict({ request: job.request, result: job.result }).catch(() => null);
|
||
document.getElementById('qs-verdict-line')?.replaceChildren(v ? verdictLine(v, { sealed: false, id: 'qs-verdict' }) : el('span', { class: 'qs-note', text: tr('frame.no_verdict') }));
|
||
}
|
||
try {
|
||
const v = await local.verify({ request: job.request, result: job.result });
|
||
const whose = v.key === serviceKey ? tr('frame.key_service') : v.signer && v.signer === mySigner ? (desktop ? tr('frame.key_computer') : tr('frame.key_browser')) : tr('frame.key_named');
|
||
document.getElementById('qs-signed-by').textContent = `${whose} ${v.signer ? v.signer.slice(0, 28) + '…' : ''}`;
|
||
check.textContent = v.valid ? tr('frame.verifies', { where: desktop ? tr('frame.here_computer') : tr('frame.here_device') }) : tr('frame.does_not_verify');
|
||
check.className = 'qs-badge ' + (v.valid ? 'ok' : 'bad');
|
||
} catch (e) { check.textContent = tr('frame.not_checked', { error: e.message }); check.className = 'qs-badge bad'; }
|
||
if (job.request?.history) {
|
||
try {
|
||
const a = await versions.checkJob(job);
|
||
account.textContent = a.ok ? tr('frame.recomputed', { version: a.version }) : `✗ ${a.problems.join('; ')}`;
|
||
account.className = 'qs-badge ' + (a.ok ? 'ok' : 'bad');
|
||
} catch (e) { account.textContent = desktop ? tr('frame.not_in_computer') : tr('frame.not_in_browser'); account.className = 'qs-badge'; account.title = e.message; }
|
||
}
|
||
}
|
||
|
||
// A history's account as a line: each hand's gates.
|
||
function writtenBy(h) {
|
||
const parts = (h.contributors || []).filter((c) => c.gates).map((c) => tr('frame.gates_by', { n: c.gates, who: `${HANDS[c.hand] || c.hand}${c.name ? ` (${c.name})` : ''}` }));
|
||
return tr('frame.written', { parts: parts.join(', ') || tr('frame.no_gates'), of: h.of?.gates ?? 0, version: h.version });
|
||
}
|
||
|
||
// ---- whose hand: the gutter and the diagram ----
|
||
let hands = null;
|
||
function showHands(b) {
|
||
hands = b || null;
|
||
if (!hands || hands.source !== text()) {
|
||
view.dispatch({ effects: [handsCol.reconfigure(hands ? [handGutter] : []), setHands.of(RangeSet.empty)] });
|
||
paintHands();
|
||
return;
|
||
}
|
||
const doc = view.state.doc;
|
||
const marks = hands.lines.flatMap((l, i) => (i < doc.lines && doc.line(i + 1).length ? [new HandMark(l.hand, `${HANDS[l.hand]} · ${l.version}${l.from ? ` (from ${l.from})` : ''}`).range(doc.line(i + 1).from)] : []));
|
||
view.dispatch({ effects: [handsCol.reconfigure([handGutter]), setHands.of(RangeSet.of(marks))] });
|
||
paintHands();
|
||
}
|
||
// Each gate drawn marked with its hand, while the diagram is of the version blamed.
|
||
function paintHands() {
|
||
const by = hands && hands.source === text() ? hands.gates || [] : [];
|
||
diagram.classList.toggle('qs-by-hand', by.length > 0);
|
||
for (const h of Object.keys(HANDS)) mark(diagram, `qc-by-${h}`, by.flatMap((x, i) => (x === h ? [i] : [])));
|
||
}
|
||
|
||
// Each gate on a held line marked on the diagram.
|
||
async function paintHeld() {
|
||
const src = text();
|
||
const r = await local.locks('held', { source: src, inputs: genInputs() }).catch(() => null);
|
||
if (text() !== src) return;
|
||
mark(diagram, 'qc-held', r?.ops || []);
|
||
rules.show(r?.held?.length || 0);
|
||
}
|
||
|
||
// ---- a proposal, reviewed change by change ----
|
||
async function offer(qasm, by, note, opts = {}) {
|
||
if (versions.busy()) { setStatus(tr('frame.choose_first'), 'bad'); return null; }
|
||
const r = await versions.review(qasm, by || { kind: 'person' }, note, opts);
|
||
if (r) {
|
||
shows.textContent = note || '';
|
||
const stops = r.blocked ? tr('frame.blocked', { n: r.blocked }) : '';
|
||
setStatus(r.accepted ? tr('frame.took', { accepted: r.accepted, offered: r.offered, who: whoName(by), stops, version: r.version }) : tr('frame.took_none', { who: whoName(by), stops, version: r.version }), 'ok');
|
||
}
|
||
return r;
|
||
}
|
||
// A program from elsewhere, recorded as where it came from.
|
||
async function from(where, f) {
|
||
if (versions.busy()) { setStatus(tr('frame.choose_first'), 'bad'); return false; }
|
||
await versions.record();
|
||
f();
|
||
await versions.record({ kind: 'origin', name: where });
|
||
return true;
|
||
}
|
||
|
||
// ---- a smell fixed: the engine's rewrite, checked the same circuit ----
|
||
async function fix(s) {
|
||
const src = text();
|
||
try {
|
||
const v = await local.variants(src);
|
||
const c = (v.candidates || []).find((x) => x.changes.length === 1 && x.changes[0] === s.message);
|
||
if (!c) { setStatus(tr('frame.no_rewrite'), 'bad'); return; }
|
||
if (text() !== src) return; // edited meanwhile: the rewrite is of an older draft
|
||
const same = sameness(c.equivalence);
|
||
const r = await offer(c.qasm, { kind: 'engine', name: 'a fix' }, `Fixed: ${s.message}`, { all: true });
|
||
if (!r) return;
|
||
if (!r.accepted) { shows.textContent = ''; setStatus(tr('frame.fix_blocked', { version: r.version }), 'bad'); return; }
|
||
shows.textContent = tr('frame.fixed_shows', { message: s.message, why: same.why || same.text });
|
||
setStatus(tr('frame.fixed', { version: r.version }), 'ok');
|
||
} catch (e) { setStatus(e.message, 'bad'); }
|
||
}
|
||
|
||
// ---- files ----
|
||
async function saveFile(name, body, type) {
|
||
if (desktop) return (await local.saveProgram(name, body)).path;
|
||
download(name, body, type);
|
||
return name;
|
||
}
|
||
function download(name, text, type) {
|
||
const a = el('a', { href: URL.createObjectURL(new Blob([text], { type })), download: name });
|
||
document.body.append(a); a.click(); a.remove();
|
||
setTimeout(() => URL.revokeObjectURL(a.href), 0);
|
||
}
|
||
async function saveJob(job) {
|
||
const name = `wai-quantum-job-${job.id || (job.result?.receipt?.digest || 'here').slice(0, 12)}.json`;
|
||
if (desktop) { const r = await local.saveJob(name, job); setStatus(tr('frame.saved', { path: r.path }), 'ok'); }
|
||
else { download(name, JSON.stringify(job, null, 2) + '\n', 'application/json'); setStatus(tr('frame.saved_job', { name }), 'ok'); }
|
||
}
|
||
saveBtn.addEventListener('click', async () => {
|
||
const name = 'program.qasm';
|
||
if (desktop) { const r = await local.saveProgram(name, text()); setStatus(tr('frame.saved', { path: r.path }), 'ok'); }
|
||
else download(name, text(), 'text/plain');
|
||
});
|
||
openBtn.addEventListener('click', () => fileIn.click());
|
||
fileIn.addEventListener('change', async () => {
|
||
const f = fileIn.files?.[0]; if (!f) return;
|
||
fileIn.value = '';
|
||
// QIR and jeff are read by the engine to OpenQASM 3, with what it noted.
|
||
const format = /\.(ll|qir)$/i.test(f.name) ? 'qir' : /\.jeff$/i.test(f.name) ? 'jeff' : null;
|
||
if (!format) {
|
||
const body = await f.text();
|
||
if (await from(`the file ${f.name}`, () => setText(body))) shows.textContent = tr('frame.opened', { name: f.name });
|
||
return;
|
||
}
|
||
let r;
|
||
try {
|
||
const request = { from: format, to: 'qasm' };
|
||
if (format === 'jeff') request.program_base64 = toBase64(new Uint8Array(await f.arrayBuffer()));
|
||
else request.program = await f.text();
|
||
r = await local.convert(request);
|
||
} catch (e) { r = { error: e.message }; }
|
||
if (r.error) { setStatus(tr('frame.open_refused', { name: f.name, format: format === 'qir' ? 'QIR' : 'jeff', error: r.error }), 'bad'); return; }
|
||
if (!(await from(`the file ${f.name}`, () => setText(r.program)))) return;
|
||
const notes = (r.notes || []).map((n) => `${n.message} [${n.code}]`);
|
||
shows.textContent = tr('frame.opened_from', { name: f.name, format: format === 'qir' ? `QIR ${r.read?.version ?? ''}`.trim() : `jeff ${r.read?.version ?? ''}`.trim(), check: r.check?.verdict?.verdict ?? 'unchecked', notes: notes.length ? notes.join('; ') : tr('frame.no_notes') });
|
||
});
|
||
shareBtn.addEventListener('click', () => {
|
||
const u = new URL(location.href); u.hash = 'qasm=' + encodeURIComponent(text()); u.search = '';
|
||
navigator.clipboard?.writeText(u.toString()).then(() => setStatus(tr('frame.copied'), 'ok'), () => setStatus(u.toString()));
|
||
});
|
||
|
||
// ---- history ----
|
||
async function showHistory() {
|
||
const all = await History.list();
|
||
historyList.replaceChildren(...(all.length ? all.slice(0, 50).map((h) => el('li', {},
|
||
el('button', { type: 'button', class: 'qs-hitem', onclick: async () => {
|
||
if (!(await from('an earlier run', () => fill(h.job.request || {})))) return;
|
||
current = { job: h.job, where: h.where, took: 0 }; show(current); setStatus(tr('frame.from_history', { where: whereWord(h.where), when: new Date(h.saved).toLocaleString(locale()) }));
|
||
} },
|
||
el('span', { class: 'qs-htitle', text: h.title || '(a program)' }),
|
||
el('span', { class: 'qs-note', text: `${METHOD_NAMES[h.job.method] || h.job.method} · ${whereWord(h.where)} · ${new Date(h.saved).toLocaleTimeString(locale())}` })))) : [el('li', { class: 'qs-note', text: tr('frame.history_empty') })]));
|
||
}
|
||
|
||
// ---- where ----
|
||
const places = [whereLocal, whereService, hw.seg];
|
||
function choose(b) {
|
||
where = b.dataset.where;
|
||
for (const x of places) { x.classList.toggle('on', x === b); x.setAttribute('aria-pressed', String(x === b)); }
|
||
hw.picker.hidden = where !== 'hardware';
|
||
loadMethods(); plan();
|
||
}
|
||
for (const b of [whereLocal, whereService]) b.addEventListener('click', () => choose(b));
|
||
async function loadMethods() {
|
||
const keep = method.value;
|
||
try {
|
||
const m = (await (where === 'service' ? service : local).methods()).methods || [];
|
||
method.replaceChildren(el('option', { value: 'auto', text: 'auto', title: tr('frame.auto_title') }),
|
||
...m.filter((x) => x.available !== false).map((x) => el('option', { value: x.method, text: tr('frame.method_reach', { method: METHOD_NAMES[x.method] || x.method, n: x.max_qubits }) })));
|
||
method.value = [...method.options].some((o) => o.value === keep) ? keep : 'auto';
|
||
} catch (e) {}
|
||
}
|
||
examples.addEventListener('change', async () => {
|
||
const e = EXAMPLES.find((x) => x.id === examples.value); if (!e) return;
|
||
examples.value = '';
|
||
if (await from(`the example ${e.title}`, () => fill(e.job))) shows.textContent = e.shows;
|
||
});
|
||
[shots, seed, method, obs, amps, mitigate, ...noiseInputs.map((n) => n[0])].forEach((i) => i.addEventListener('input', () => { clearTimeout(drawTimer); drawTimer = setTimeout(plan, 250); }));
|
||
runBtn.addEventListener('click', run);
|
||
cancelBtn.addEventListener('click', cancel);
|
||
laterBtn.addEventListener('click', () => running?.later?.());
|
||
|
||
// ---- asking ----
|
||
const model = () => {
|
||
const endpoint = aiEndpoint.value.trim(), name = aiModel.value.trim();
|
||
keepConf();
|
||
return desktop ? new DesktopModel(endpoint, name) : new HttpModel(endpoint, name);
|
||
};
|
||
let assistant = null;
|
||
let aiConnected = false; // the model server answered: Alternatives may ask it
|
||
const assistantFor = () => {
|
||
const m = model();
|
||
if (!assistant || assistant.model.endpoint !== m.endpoint || assistant.model.name !== m.name) {
|
||
assistant = new Assistant({ engine: local, model: m, context: () => ({ qasm: text(), job: request(), last: current?.job }), stance: () => (organic.on() ? 'questions' : !flags.on('assistant_asks') ? 'suggest' : flags.held('assistant_asks') ? 'questions' : aiStance.value), rules: brief });
|
||
}
|
||
return assistant;
|
||
};
|
||
const say = (cls, ...kids) => { const li = el('li', { class: cls }, ...kids); aiLog.append(li); li.scrollIntoView({ block: 'nearest' }); return li; };
|
||
// An answer's text, its code in blocks.
|
||
// **bold** and `code` within a line, built as nodes, never as HTML.
|
||
const inline = (line) => line.split(/(\*\*[^*]+\*\*|`[^`]+`)/).filter(Boolean).map((t) =>
|
||
t.startsWith('**') && t.endsWith('**') ? el('strong', { text: t.slice(2, -2) }) : t.startsWith('`') && t.endsWith('`') ? el('code', { text: t.slice(1, -1) }) : document.createTextNode(t.replace(/\$([^$]+)\$/g, '$1')));
|
||
const prose = (t) => t.split(/```[a-z0-9]*\n?/i).map((part, i) => (i % 2 ? el('pre', { class: 'qs-ai-code', text: part.replace(/\n$/, '') }) : part.trim() ? el('p', { class: 'qs-ai-text' }, ...part.trim().split('\n').flatMap((l, j) => [j ? el('br') : null, ...inline(l)])) : null));
|
||
async function ask(q, label = q) {
|
||
if (aiSend.disabled) return;
|
||
if (locale() === 'zh-CN') q += '\n\nAnswer in Simplified Chinese.';
|
||
aiSend.disabled = true;
|
||
say('qs-ai-user', el('p', { text: label }));
|
||
const thinking = say('qs-ai-tool', el('span', { class: 'qs-note', text: tr('ai.thinking') }));
|
||
try {
|
||
await assistantFor().ask(q, (ev) => {
|
||
if (ev.type === 'tool') {
|
||
const res = JSON.stringify(ev.result);
|
||
aiLog.insertBefore(el('li', { class: 'qs-ai-tool' }, el('details', {}, el('summary', { text: tr('ai.used', { name: ev.name }) }), el('pre', { class: 'qs-ai-code', text: res.length > 2000 ? res.slice(0, 2000) + '…' : res }))), thinking);
|
||
} else if (ev.type === 'retry') {
|
||
aiLog.insertBefore(el('li', { class: 'qs-ai-tool' }, el('span', { class: 'qs-note', text: tr('ai.retry', { error: ev.error }) })), thinking);
|
||
} else if (ev.type === 'answer') {
|
||
thinking.remove();
|
||
aiConnected = true;
|
||
const li = say('qs-ai-answer', ...prose(ev.text));
|
||
for (const p of ev.proposals) {
|
||
const box = el('div', { class: 'qs-ai-proposal' });
|
||
// The correctness record, when the reader keeps one: what the engine made of it.
|
||
let rid = null;
|
||
telemetry.record(proposalRecord(p, q)).then((i) => { rid = i; });
|
||
// Every proposal states the level the engine checked it to, never "valid".
|
||
if (p.verdict) box.append(levelBadge(p.verdict), ' ');
|
||
if (!p.lint.ok) box.append(el('span', { class: 'qs-badge bad', text: tr('ai.does_not_parse') }), el('span', { class: 'qs-note', text: ' ' + p.lint.error }));
|
||
else {
|
||
const r = p.job?.result;
|
||
const counts = r?.counts ? Object.entries(r.counts).sort((a, b) => b[1] - a[1]).slice(0, 4).map(([k, n]) => `${compact(k)}: ${n}`).join(', ') : '';
|
||
box.append(el('span', { class: 'qs-badge ok', text: tr('ai.checked', { qubits: p.lint.qubits, gates: p.lint.gates }) }),
|
||
el('span', { class: 'qs-note', text: p.job ? tr('ai.ran_here', { method: METHOD_NAMES[p.job.method] || p.job.method }) + (counts ? '; ' + counts : '') : p.error ? tr('ai.did_not_run', { error: p.error }) : '' }),
|
||
organic.on()
|
||
? el('span', { class: 'qs-note qs-ai-held', text: tr('ai.organic') })
|
||
: el('button', { class: 'qs-btn qs-small qs-ai-apply', type: 'button', text: tr('ai.apply'), onclick: () => { telemetry.decide(rid, 'accepted'); offer(p.qasm, { kind: 'assistant', name: assistant?.model?.name || undefined }, 'The assistant’s program, checked and run before it was offered.'); } }));
|
||
}
|
||
box.append(' ', el('button', { class: 'qs-btn qs-small qs-ai-reject', type: 'button', text: 'Not this', onclick: (e) => { telemetry.decide(rid, 'rejected'); e.target.disabled = true; box.classList.add('qs-set-aside'); } }));
|
||
li.append(box);
|
||
}
|
||
if (ev.unsourced?.length) li.append(el('p', { class: 'qs-ai-unsourced', text: tr('ai.unsourced', { list: ev.unsourced.join(', ') }) }));
|
||
if (ev.usage?.total_tokens) li.append(el('p', { class: 'qs-note', text: tr('ai.tokens', { into: ev.usage.prompt_tokens ?? '?', out: ev.usage.completion_tokens ?? '?' }) }));
|
||
}
|
||
});
|
||
} catch (e) {
|
||
thinking.remove();
|
||
say('qs-ai-error', el('p', { text: tr('ai.error', { error: e.message }) }));
|
||
aiState.textContent = tr('ai.not_connected'); aiState.className = 'qs-badge bad';
|
||
aiConnected = false;
|
||
} finally { aiSend.disabled = false; }
|
||
}
|
||
// A proposal's correctness record: its codes, its verdict, its repairs and its receipt.
|
||
function proposalRecord(p, task) {
|
||
const v = p.verdict || {};
|
||
const codes = [p.lint?.code, ...(v.refusal?.codes || []), ...(v.parsed?.diagnostics || []).map((d) => d.code)].filter(Boolean);
|
||
return {
|
||
model: assistant?.model?.name || null, task, program: p.qasm, intent: p.job?.request?.intent, run: p.job?.request ? { shots: p.job.request.shots } : undefined,
|
||
codes: [...new Set(codes)], verdict: p.verdict ? { level: v.level, holds: v.holds } : undefined, repairs: (v.repairs || []).map((r) => r.code),
|
||
receipt: p.job?.result?.receipt?.digest, decision: 'pending',
|
||
};
|
||
}
|
||
|
||
// ---- answers in the answer view layer: drawn from a pasted answer, or as the model streams one ----
|
||
let avContext = { task: null, model: null };
|
||
async function drawAnswer(t) {
|
||
answers.reset(); avAfter.hidden = true; avContext = { task: null, model: null };
|
||
avStatus.textContent = 'Drawing as it arrives…';
|
||
for (const line of t.split(/(?<=\n)/)) await answers.feed(line);
|
||
await answers.end();
|
||
}
|
||
async function askAnswer() {
|
||
const q = avQ.value.trim();
|
||
if (!q) return;
|
||
answers.reset(); avAfter.hidden = true;
|
||
avContext = { task: q, model: aiModel.value.trim() || null };
|
||
avStatus.textContent = 'Asking the model…';
|
||
try {
|
||
const { prompt } = await local.answer('prompt', {});
|
||
await model().stream({ model: aiModel.value.trim() || undefined, temperature: 0, messages: [
|
||
{ role: 'system', content: `${prompt}\nWrite only the answer, in the layer; prose goes in Text views.` },
|
||
{ role: 'user', content: `${q}\n\n(The program in the editor:)\n\`\`\`qasm\n${text()}\`\`\`` },
|
||
] }, (piece) => answers.feed(piece));
|
||
await answers.end();
|
||
} catch (e) { avStatus.textContent = `The model could not answer: ${e.message}`; }
|
||
}
|
||
async function answerEnded({ answer, text: t, values }) {
|
||
if (!answer || answer.error) return;
|
||
const n = answer.errors;
|
||
avStatus.textContent = `${answer.statements.length} statements; ${n} error${n === 1 ? '' : 's'}${answer.warnings ? ` and ${answer.warnings} note${answer.warnings === 1 ? '' : 's'}` : ''}.`;
|
||
const rid = await telemetry.record({ ...answerRecord({ answer, values }), task: avContext.task, model: avContext.model });
|
||
const seal = answer.statements.find((s) => s.view === 'Receipt' && values.get(s.name)?.ok);
|
||
avAfter.replaceChildren(
|
||
el('button', { class: 'qs-btn qs-small', type: 'button', id: 'qs-av-accept', text: 'Take it', onclick: () => { telemetry.decide(rid, 'accepted'); avStatus.textContent = 'Taken.'; } }),
|
||
el('button', { class: 'qs-btn qs-small', type: 'button', id: 'qs-av-reject', text: 'Not this', onclick: () => { telemetry.decide(rid, 'rejected'); avStatus.textContent = 'Set aside.'; } }),
|
||
seal ? el('button', { class: 'qs-btn qs-small', type: 'button', id: 'qs-av-store', text: 'Save with its receipt', onclick: async () => {
|
||
const m = await local.answer('marker', { statement: seal.name, sealed: values.get(seal.name).value });
|
||
saveFile('answer.wqa', t.replace(/\n?$/, '\n') + m.marker + '\n', 'text/plain');
|
||
} }) : null);
|
||
avAfter.hidden = false;
|
||
}
|
||
avDraw.addEventListener('click', () => drawAnswer(avInput.value));
|
||
avAsk.addEventListener('click', askAnswer);
|
||
|
||
aiSend.addEventListener('click', () => { const q = aiInput.value.trim(); if (q) { aiInput.value = ''; ask(q); } });
|
||
aiInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); aiSend.click(); } });
|
||
aiConnect.addEventListener('click', async () => {
|
||
aiState.textContent = tr('ai.connecting'); aiState.className = 'qs-badge run';
|
||
try {
|
||
const ids = await model().models();
|
||
const name = ids[0] ? ids[0].split(/[\\/]/).pop().replace(/\.gguf$/i, '') : '';
|
||
aiState.textContent = tr('ai.connected') + (name ? ' · ' + name : ''); aiState.className = 'qs-badge ok'; aiState.title = ids[0] || '';
|
||
aiConnected = true;
|
||
if (!aiModel.value && ids[0]) aiModel.value = ids[0];
|
||
} catch (e) { aiState.textContent = tr('ai.not_connected'); aiState.className = 'qs-badge bad'; aiState.title = e.message; aiConnected = false; }
|
||
});
|
||
|
||
// ---- start ----
|
||
const handed = new URLSearchParams(location.hash.slice(1)).get('qasm');
|
||
let draft = null;
|
||
try { draft = localStorage.getItem(DRAFT); } catch (e) {}
|
||
if (handed) { fill({ qasm: handed }); shows.textContent = tr('frame.from_link'); }
|
||
else if (draft) setText(draft);
|
||
else { fill(EXAMPLES[0].job); shows.textContent = EXAMPLES[0].shows; }
|
||
// The draft is kept as the page goes, not only once a redraw has waited.
|
||
addEventListener('pagehide', () => { try { localStorage.setItem(DRAFT, text()); } catch (e) {} });
|
||
// The versions this browser kept go on if the editor holds their program; a
|
||
// draft typed since is the reader's, and anything else starts where it came from.
|
||
versions.begin(handed ? { kind: 'origin', name: 'a link' } : draft ? { kind: 'person' } : { kind: 'origin', name: `the example ${EXAMPLES[0].title}` },
|
||
draft && !handed ? { kind: 'origin', name: 'the draft kept here' } : null).catch((e) => setStatus(tr('frame.versions_error', { error: e.message }), 'bad'));
|
||
loadMethods();
|
||
showHistory();
|
||
// Jobs left at a provider, from this window or an earlier one, are followed;
|
||
// each one's result goes to the history when it finishes.
|
||
if (hardware) {
|
||
hardware.onFinished = async (body) => { await keep(body); setStatus(tr('hardware.finished', { id: body.id, provider: body.request?.provider }), 'ok'); };
|
||
hardware.onEnded = (job) => setStatus(tr('hardware.ended', { id: job.id, provider: job.request?.provider, why: job.error || job.status }), 'bad');
|
||
hardware.resume();
|
||
}
|
||
local.signer().then((d) => { mySigner = d; document.getElementById('qs-signer').textContent = tr('frame.signs_as', { did: d.slice(0, 32), whose: desktop ? tr('frame.key_computer') : tr('frame.key_made_browser') }); }).catch(() => {});
|
||
service?.describe().then((d) => { serviceKey = d.receipt_key || null; }).catch(() => { whereService.disabled = true; whereService.title = tr('frame.service_silent'); });
|
||
edited();
|
||
forceLinting(view);
|
||
// `propose(qasm, by, note)`: a program offered as `by`'s proposal, taken
|
||
// change by change; it resolves once the reader has chosen.
|
||
return { view, run, fill, request, gallery, versions, rules, debug, course, propose: (qasm, by, note) => offer(qasm, by, note) };
|
||
}
|
||
|
||
const root = document.getElementById('studio');
|
||
if (root) window.quantumStudio = mount(root);
|