// Interactive view: step-through generation. Session id lives here; the // tracer lives server-side. Every action re-renders from the canonical // payload (status, text, candidates, preview, step count). import { call, stream } from '../api.js'; import { renderPlot, clearPlot } from '../plot.js'; import { createPromptEditor, createSamplingControls } from '../prompt-editor.js'; import { button, checkbox, details, el, escapeHTML, field, numberInput, outputText, saveJSON, segmented, statusPane, } from '../ui.js'; export function createInteractiveView() { let sessionId = null; let running = false; const editor = createPromptEditor({ promptDefault: 'The future of artificial intelligence is', }); const sampling = createSamplingControls(); const logTopK = numberInput({ value: 10, min: 1 }); const logFullProbs = checkbox('Log full probabilities', { hint: 'Whole-vocabulary distribution per step (large exports)', }); const stopAtEos = checkbox('Stop at EOS', { checked: true }); const initBtn = button('Initialize', { variant: 'primary' }); const resetBtn = button('Reset'); const status = statusPane({ lines: 2 }); const output = outputText({ rows: 6, label: 'Output' }); const continueTokens = numberInput({ value: 10, min: 1, max: 1000 }); const runBtn = button('Run'); const stopBtn = button('Stop', { variant: 'danger' }); stopBtn.disabled = true; // --------------------------------------------------- candidates + preview const candidatesBody = el('tbody'); const candidatesTable = el('div', { class: 'table-scroll candidates-scroll' }, el('table', { class: 'data-table' }, el('thead', {}, el('tr', {}, el('th', {}, ''), el('th', {}, 'Rank'), el('th', {}, 'Token ID'), el('th', {}, 'Token'), el('th', {}, 'p'), el('th', {}, 'p (raw)'))), candidatesBody)); const useOverride = checkbox('Token ID override', { hint: 'Specify an arbitrary token ID instead of selecting from the list', }); const overrideId = numberInput({ value: 0, min: 0 }); const overrideField = field('Token ID', overrideId); overrideField.hidden = true; useOverride.input.addEventListener('change', () => { overrideField.hidden = !useOverride.input.checked; candidatesTable.classList.toggle('is-disabled', useOverride.input.checked); }); let selectedTokenId = null; function renderCandidates(candidates, previewId) { selectedTokenId = previewId; candidatesBody.replaceChildren(...candidates.map((c) => { const radio = el('input', { type: 'radio', name: 'jc-candidate', value: String(c.token_id) }); radio.checked = c.token_id === previewId; radio.addEventListener('change', () => { selectedTokenId = c.token_id; }); return el('tr', { class: c.token_id === previewId ? 'is-preview' : '' }, el('td', {}, radio), el('td', {}, String(c.rank)), el('td', { class: 'font-mono' }, String(c.token_id)), el('td', { class: 'font-mono token-cell' }, c.text), el('td', { class: 'font-mono' }, c.prob.toFixed(4)), el('td', { class: 'font-mono' }, c.raw_prob.toFixed(4)), ); })); } const undoBtn = button('Step back (undo)'); const stepBtn = button('Next step ▸', { variant: 'primary' }); // -------------------------------------------------------- lens panel const lensMode = segmented([ { value: 'logit', label: 'Logit' }, { value: 'jacobian', label: 'Jacobian' }, { value: 'diff', label: 'Diff (J − logit)' }, ], { value: 'logit', small: true }); const lensStride = numberInput({ value: 1, min: 1 }); const lensTopK = numberInput({ value: 50, min: 1 }); const lensRefresh = button('Refresh lens', { size: 'sm' }); const lensApplyIv = button('Apply Lens-view interventions to this session', { size: 'sm' }); const lensStatus = statusPane({ lines: 1 }); const lensPlot = el('div', { class: 'plot-host' }); const lensPanel = details('Layer lens (current position) — experimental', el('div', { class: 'stack' }, el('p', { class: 'field-hint' }, 'Per-layer readout of the next-token position via the logit or Jacobian ' + 'lens. Refresh runs one extra forward pass. ⚠ Experimental — readouts ' + 'may currently yield nonsense.'), el('div', { class: 'row-wrap' }, field('Lens', lensMode), field('Layer stride', lensStride), field('Readouts per layer', lensTopK)), el('div', { class: 'btn-row' }, lensRefresh, lensApplyIv), lensStatus, lensPlot, )); // -------------------------------------------------- navigation + export const gotoStep = numberInput({ value: 0, min: 0 }); const stepDisplay = el('span', { class: 'pill-value font-mono' }, '0'); const gotoBtn = button('Go to step'); const exportBtn = button('Download JSON'); exportBtn.disabled = true; const root = el('div', { class: 'view-grid' }, el('section', { class: 'card' }, el('h2', { class: 'card-title' }, 'Prompt'), editor.root, details('Sampling', sampling.root, { open: true }), details('Advanced (logging & stopping)', el('div', { class: 'row-wrap' }, field('Log top-K tokens', logTopK), logFullProbs, stopAtEos)), el('div', { class: 'btn-row' }, initBtn, resetBtn), status, output, el('div', { class: 'row-wrap row-end' }, field('Continue for N tokens', continueTokens), el('div', { class: 'btn-row' }, runBtn, stopBtn)), ), el('section', { class: 'card' }, el('h2', { class: 'card-title' }, 'Token selection'), el('p', { class: 'card-sub' }, 'The highlighted row is exactly the token Next step will commit — ' + 'for sampling, the draw happens at preview time.'), candidatesTable, el('div', { class: 'row-wrap' }, useOverride, overrideField), el('div', { class: 'btn-row' }, undoBtn, stepBtn), lensPanel, el('h2', { class: 'card-title', style: 'margin-top:1.25rem' }, 'Navigation & export'), el('div', { class: 'row-wrap row-end' }, field('Target step (0 = initial)', gotoStep), el('div', { class: 'pill' }, el('span', { class: 'pill-label' }, 'Current step'), stepDisplay), el('div', { class: 'btn-row' }, gotoBtn, exportBtn)), ), ); // ----------------------------------------------------------------- state function samplingArgs() { return [sampling.strategy(), sampling.temperature(), sampling.topK(), sampling.topP()]; } function apply(payload) { if (!payload.ok) { status.set(payload.error, 'error'); return; } status.set(payload.status); output.set(payload.text); sessionId = payload.session_id; stepDisplay.textContent = String(payload.step ?? 0); exportBtn.disabled = !sessionId; if (payload.eos) { renderCandidates([], null); candidatesBody.replaceChildren(el('tr', {}, el('td', { colspan: 6, class: 'eos-cell' }, 'EOS — generation complete'))); } else if (payload.candidates) { renderCandidates(payload.candidates, payload.preview_id); } } initBtn.addEventListener('click', async () => { status.set('Initializing…', 'busy'); apply(await call('interactive_init', [ ...editor.values(), ...samplingArgs(), num(logTopK), ]).catch(errPayload)); }); resetBtn.addEventListener('click', async () => { apply(await call('interactive_reset', [sessionId]).catch(errPayload)); candidatesBody.replaceChildren(); clearPlot(lensPlot); lensStatus.set(''); }); stepBtn.addEventListener('click', async () => { status.set('Stepping…', 'busy'); apply(await call('interactive_step', [ sessionId, ...samplingArgs(), selectedTokenId, useOverride.input.checked, num(overrideId), num(logTopK), logFullProbs.input.checked, stopAtEos.input.checked, ]).catch(errPayload)); }); undoBtn.addEventListener('click', async () => { apply(await call('interactive_undo', [ sessionId, ...samplingArgs(), num(logTopK), ]).catch(errPayload)); }); gotoBtn.addEventListener('click', async () => { apply(await call('interactive_goto', [ sessionId, num(gotoStep), ...samplingArgs(), num(logTopK), ]).catch(errPayload)); }); runBtn.addEventListener('click', async () => { if (running) return; running = true; runBtn.disabled = true; stepBtn.disabled = true; stopBtn.disabled = false; try { const final = await stream('interactive_continue', [ sessionId, ...samplingArgs(), num(continueTokens), num(logTopK), logFullProbs.input.checked, stopAtEos.input.checked, ], (chunk) => { if (chunk.type === 'progress') { status.set(chunk.status, 'busy'); output.set(chunk.text); stepDisplay.textContent = String(chunk.step); } }); apply(final); } catch (e) { status.set(`Error: ${escapeHTML(e.message)}`, 'error'); } finally { running = false; runBtn.disabled = false; stepBtn.disabled = false; stopBtn.disabled = true; } }); stopBtn.addEventListener('click', async () => { stopBtn.disabled = true; await call('interactive_stop', [sessionId]).catch(() => {}); }); exportBtn.addEventListener('click', async () => { const res = await call('interactive_export', [sessionId, ...samplingArgs()]) .catch(errPayload); if (!res.ok) { status.set(res.error, 'error'); return; } saveJSON(res.export, 'jacobina-interactive.json'); }); lensRefresh.addEventListener('click', async () => { lensStatus.set('Computing…', 'busy'); const res = await call('interactive_lens', [ sessionId, lensMode.value(), num(lensStride), num(lensTopK), ]).catch(errPayload); if (!res.ok) { lensStatus.set(res.error, 'error'); clearPlot(lensPlot); return; } lensStatus.set(res.status); renderPlot(lensPlot, res.figure); }); lensApplyIv.addEventListener('click', async () => { const res = await call('interactive_apply_interventions', [sessionId]) .catch(errPayload); lensStatus.set(res.ok ? res.status : res.error, res.ok ? '' : 'error'); }); return { root }; } function num(input) { const v = parseFloat(input.value); return Number.isNaN(v) ? null : v; } function errPayload(e) { return { ok: false, error: e.message || String(e) }; }