diff --git a/static/ast-parser.js b/static/ast-parser.js index a0ee43c..c0988c7 100644 --- a/static/ast-parser.js +++ b/static/ast-parser.js @@ -212,7 +212,7 @@ function buildInstrument(node) { function buildVariation(node) { const v = { type: 'variation', - dependsOn: node.props.depends_on ?? null, + dependsOn: node.props.depends_on ?? node.props.for_value ?? null, basicProperties: null, labelSpecs: [], subvariations: [], diff --git a/static/components/EnvelopeEditor.js b/static/components/EnvelopeEditor.js index 6586269..de91853 100644 --- a/static/components/EnvelopeEditor.js +++ b/static/components/EnvelopeEditor.js @@ -24,12 +24,9 @@ export const EnvelopeEditor = { setup(props) { function toggle(section) { const bp = props.basicProperties; - if (bp[section]) { - bp[section] = null; - } else { - bp[section] = defaultShape(section); - } - props.onChange?.(); + const old = bp[section]; + bp[section] = old ? null : defaultShape(section); + props.onChange?.({ undo: () => { bp[section] = old; } }); } return () => { @@ -57,7 +54,7 @@ export const EnvelopeEditor = { ? h('div', { class: disabled ? 'se-envelope-disabled' : null }, [ h(ShapeEditor, { shape: bp[key], - onChange: props.onChange, + onChange: info => props.onChange?.(info), }), ]) : null, diff --git a/static/components/PaneFO.js b/static/components/PaneFO.js index c0c347e..2394dca 100644 --- a/static/components/PaneFO.js +++ b/static/components/PaneFO.js @@ -39,11 +39,11 @@ export const PaneFO = { // Returns a change handler that intercepts the first edit to a linked // instrument and shows the embed-or-discard modal before applying it. + // `info` may carry { undo } forwarded from ShapeEditor / EnvelopeEditor. function makeChangeHandler(instr, apply) { - return () => { + return (info) => { if (instr.isLinked && !instr.isDirty) { - // Stash the apply callback and show the modal. - pendingEdit.value = { instr, apply }; + pendingEdit.value = { instr, apply, undo: info?.undo }; } else { apply(); instr.isDirty = true; @@ -62,6 +62,7 @@ export const PaneFO = { } function discardEdit() { + pendingEdit.value?.undo?.(); pendingEdit.value = null; } @@ -104,8 +105,13 @@ export const PaneFO = { children.push( h('h4', { style: 'margin:0 0 0.5rem' }, 'Variation'), h(ObjectExtended, { fields: variationFields(node), onChange: ({ key, value }) => { - if (key === 'depends_on') node.dependsOn = value; - onChange(); + if (key === 'depends_on') { + const old = node.dependsOn; + node.dependsOn = value; + onChange({ undo: () => { node.dependsOn = old; } }); + } else { + onChange({}); + } }}), node.basicProperties ? h(EnvelopeEditor, { basicProperties: node.basicProperties, onChange }) diff --git a/static/components/ShapeEditor.js b/static/components/ShapeEditor.js index 3f23e26..7ebdef9 100644 --- a/static/components/ShapeEditor.js +++ b/static/components/ShapeEditor.js @@ -1,7 +1,8 @@ import { h } from 'vue'; // Renders a shape's coord table with cascade-shift and an SVG preview. -// `shape` is mutated in place; `onChange` called after each mutation. +// `shape` is mutated in place; `onChange` called after each mutation with +// { undo } so callers can revert if needed. export const ShapeEditor = { props: ['shape', 'onChange'], @@ -14,27 +15,31 @@ export const ShapeEditor = { const delta = num - old; coord[field] = num; - // cascade-shift: if x increased past next coord, shift all following + const shifted = []; if (field === 'x' && delta > 0) { for (let j = i + 1; j < props.shape.coords.length; j++) { if (props.shape.coords[j].x <= num) { + shifted.push({ j, ox: props.shape.coords[j].x }); props.shape.coords[j].x += delta; } else break; } } - props.onChange?.(); + props.onChange?.({ undo: () => { + coord[field] = old; + for (const { j, ox } of shifted) props.shape.coords[j].x = ox; + }}); } function addCoord() { const coords = props.shape.coords; const lastX = coords.length ? coords[coords.length - 1].x + 1 : 1; coords.push({ x: lastX, y: 0, z: 1, isSharp: false }); - props.onChange?.(); + props.onChange?.({ undo: () => coords.pop() }); } function removeCoord(i) { - props.shape.coords.splice(i, 1); - props.onChange?.(); + const removed = props.shape.coords.splice(i, 1)[0]; + props.onChange?.({ undo: () => props.shape.coords.splice(i, 0, removed) }); } function renderSvg() { @@ -55,19 +60,14 @@ export const ShapeEditor = { return [sx, sy]; }; - const points = coords.map(c => toSvg(c).join(',') ).join(' '); + const points = coords.map(c => toSvg(c).join(',')).join(' '); return h('svg', { class: 'se-shape-svg', viewBox: `0 0 ${W} ${H}`, preserveAspectRatio: 'none', }, [ - h('polyline', { - points, - fill: 'none', - stroke: '#2a6aaa', - 'stroke-width': '1.5', - }), + h('polyline', { points, fill: 'none', stroke: '#2a6aaa', 'stroke-width': '1.5' }), ...coords.map(c => { const [sx, sy] = toSvg(c); return h('circle', { cx: sx, cy: sy, r: 2.5, fill: '#6aacff' }); @@ -101,7 +101,11 @@ export const ShapeEditor = { })), h('td', null, h('input', { type: 'checkbox', checked: !!coord.isSharp, - onChange: e => { coord.isSharp = e.target.checked; props.onChange?.(); }, + onChange: e => { + const old = coord.isSharp; + coord.isSharp = e.target.checked; + props.onChange?.({ undo: () => { coord.isSharp = old; } }); + }, })), h('td', null, h('button', { onClick: () => removeCoord(i) }, '✕')), ]) diff --git a/static/components/StatusPoller.js b/static/components/StatusPoller.js index 7e4c860..fa6243c 100644 --- a/static/components/StatusPoller.js +++ b/static/components/StatusPoller.js @@ -1,35 +1,20 @@ -import { h, ref, onMounted, onUnmounted } from 'vue'; +import { h, onMounted, onUnmounted, ref } from 'vue'; import { fetchStatus } from '../api.js'; export const StatusPoller = { props: ['store'], setup(props) { const timer = ref(null); - const eta = ref(null); - - function nextInterval(status) { - if (!status) return 2000; - const pct = status.progress ?? 0; - // At 0–20%: poll at centile-of-ETA intervals - if (eta.value && pct > 0 && pct <= 20) { - return Math.max(500, (eta.value * 10) | 0); - } - return 2000; - } async function poll() { try { const status = await fetchStatus(props.store.credentials); - if (status.eta) eta.value = status.eta; props.store.synthesisStatus = status; - if (status.frozen) { - // done — stop polling - return; - } + if (status.frozen) return; } catch (_) { - // transient error — keep polling + // transient — keep polling } - timer.value = setTimeout(poll, nextInterval(props.store.synthesisStatus)); + timer.value = setTimeout(poll, 2000); } onMounted(() => { poll(); }); @@ -39,10 +24,16 @@ export const StatusPoller = { const s = props.store.synthesisStatus; if (!s) return h('div', { class: 'se-status' }, 'Polling…'); - const pct = s.progress ?? 0; - const label = s.frozen - ? (s.error ? `Error: ${s.error}` : 'Done') - : `${s.state ?? 'Running'} ${pct}%`; + const total = s.notes_in_total ?? 0; + const done = s.currently_rendered_notes ?? 0; + const pct = total > 0 ? Math.min(100, Math.round(done / total * 100)) : 0; + + let label; + if (s.frozen) { + label = s.errors ? `Error: ${s.errors}` : 'Done'; + } else { + label = s.remaining_time ?? `Synthesizing… ${pct}%`; + } return h('div', { class: 'se-status' }, [ h('span', null, label),