forked from flow/vue3js-app-proposal-for-sdk-claude
Fix subvariation display, progress bar, and linked-instrument discard
- ast-parser: read for_value= prop for sub-variations (was always null,
causing fallback to positional "subvariation N" label)
- StatusPoller: calculate progress from currently_rendered_notes /
notes_in_total; show remaining_time string from status.json
- ShapeEditor/EnvelopeEditor: pass { undo } through onChange so callers
can revert in-place mutations
- PaneFO: makeChangeHandler stashes undo from info arg; discardEdit
calls undo() before clearing pendingEdit; depends_on handler provides
its own undo for the inline node.dependsOn mutation
This commit is contained in:
parent
2060f109b6
commit
4c392927cf
@ -212,7 +212,7 @@ function buildInstrument(node) {
|
|||||||
function buildVariation(node) {
|
function buildVariation(node) {
|
||||||
const v = {
|
const v = {
|
||||||
type: 'variation',
|
type: 'variation',
|
||||||
dependsOn: node.props.depends_on ?? null,
|
dependsOn: node.props.depends_on ?? node.props.for_value ?? null,
|
||||||
basicProperties: null,
|
basicProperties: null,
|
||||||
labelSpecs: [],
|
labelSpecs: [],
|
||||||
subvariations: [],
|
subvariations: [],
|
||||||
|
|||||||
@ -24,12 +24,9 @@ export const EnvelopeEditor = {
|
|||||||
setup(props) {
|
setup(props) {
|
||||||
function toggle(section) {
|
function toggle(section) {
|
||||||
const bp = props.basicProperties;
|
const bp = props.basicProperties;
|
||||||
if (bp[section]) {
|
const old = bp[section];
|
||||||
bp[section] = null;
|
bp[section] = old ? null : defaultShape(section);
|
||||||
} else {
|
props.onChange?.({ undo: () => { bp[section] = old; } });
|
||||||
bp[section] = defaultShape(section);
|
|
||||||
}
|
|
||||||
props.onChange?.();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@ -57,7 +54,7 @@ export const EnvelopeEditor = {
|
|||||||
? h('div', { class: disabled ? 'se-envelope-disabled' : null }, [
|
? h('div', { class: disabled ? 'se-envelope-disabled' : null }, [
|
||||||
h(ShapeEditor, {
|
h(ShapeEditor, {
|
||||||
shape: bp[key],
|
shape: bp[key],
|
||||||
onChange: props.onChange,
|
onChange: info => props.onChange?.(info),
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
: null,
|
: null,
|
||||||
|
|||||||
@ -39,11 +39,11 @@ export const PaneFO = {
|
|||||||
|
|
||||||
// Returns a change handler that intercepts the first edit to a linked
|
// Returns a change handler that intercepts the first edit to a linked
|
||||||
// instrument and shows the embed-or-discard modal before applying it.
|
// instrument and shows the embed-or-discard modal before applying it.
|
||||||
|
// `info` may carry { undo } forwarded from ShapeEditor / EnvelopeEditor.
|
||||||
function makeChangeHandler(instr, apply) {
|
function makeChangeHandler(instr, apply) {
|
||||||
return () => {
|
return (info) => {
|
||||||
if (instr.isLinked && !instr.isDirty) {
|
if (instr.isLinked && !instr.isDirty) {
|
||||||
// Stash the apply callback and show the modal.
|
pendingEdit.value = { instr, apply, undo: info?.undo };
|
||||||
pendingEdit.value = { instr, apply };
|
|
||||||
} else {
|
} else {
|
||||||
apply();
|
apply();
|
||||||
instr.isDirty = true;
|
instr.isDirty = true;
|
||||||
@ -62,6 +62,7 @@ export const PaneFO = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function discardEdit() {
|
function discardEdit() {
|
||||||
|
pendingEdit.value?.undo?.();
|
||||||
pendingEdit.value = null;
|
pendingEdit.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,8 +105,13 @@ export const PaneFO = {
|
|||||||
children.push(
|
children.push(
|
||||||
h('h4', { style: 'margin:0 0 0.5rem' }, 'Variation'),
|
h('h4', { style: 'margin:0 0 0.5rem' }, 'Variation'),
|
||||||
h(ObjectExtended, { fields: variationFields(node), onChange: ({ key, value }) => {
|
h(ObjectExtended, { fields: variationFields(node), onChange: ({ key, value }) => {
|
||||||
if (key === 'depends_on') node.dependsOn = value;
|
if (key === 'depends_on') {
|
||||||
onChange();
|
const old = node.dependsOn;
|
||||||
|
node.dependsOn = value;
|
||||||
|
onChange({ undo: () => { node.dependsOn = old; } });
|
||||||
|
} else {
|
||||||
|
onChange({});
|
||||||
|
}
|
||||||
}}),
|
}}),
|
||||||
node.basicProperties
|
node.basicProperties
|
||||||
? h(EnvelopeEditor, { basicProperties: node.basicProperties, onChange })
|
? h(EnvelopeEditor, { basicProperties: node.basicProperties, onChange })
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { h } from 'vue';
|
import { h } from 'vue';
|
||||||
|
|
||||||
// Renders a shape's coord table with cascade-shift and an SVG preview.
|
// 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 = {
|
export const ShapeEditor = {
|
||||||
props: ['shape', 'onChange'],
|
props: ['shape', 'onChange'],
|
||||||
@ -14,27 +15,31 @@ export const ShapeEditor = {
|
|||||||
const delta = num - old;
|
const delta = num - old;
|
||||||
coord[field] = num;
|
coord[field] = num;
|
||||||
|
|
||||||
// cascade-shift: if x increased past next coord, shift all following
|
const shifted = [];
|
||||||
if (field === 'x' && delta > 0) {
|
if (field === 'x' && delta > 0) {
|
||||||
for (let j = i + 1; j < props.shape.coords.length; j++) {
|
for (let j = i + 1; j < props.shape.coords.length; j++) {
|
||||||
if (props.shape.coords[j].x <= num) {
|
if (props.shape.coords[j].x <= num) {
|
||||||
|
shifted.push({ j, ox: props.shape.coords[j].x });
|
||||||
props.shape.coords[j].x += delta;
|
props.shape.coords[j].x += delta;
|
||||||
} else break;
|
} else break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
props.onChange?.();
|
props.onChange?.({ undo: () => {
|
||||||
|
coord[field] = old;
|
||||||
|
for (const { j, ox } of shifted) props.shape.coords[j].x = ox;
|
||||||
|
}});
|
||||||
}
|
}
|
||||||
|
|
||||||
function addCoord() {
|
function addCoord() {
|
||||||
const coords = props.shape.coords;
|
const coords = props.shape.coords;
|
||||||
const lastX = coords.length ? coords[coords.length - 1].x + 1 : 1;
|
const lastX = coords.length ? coords[coords.length - 1].x + 1 : 1;
|
||||||
coords.push({ x: lastX, y: 0, z: 1, isSharp: false });
|
coords.push({ x: lastX, y: 0, z: 1, isSharp: false });
|
||||||
props.onChange?.();
|
props.onChange?.({ undo: () => coords.pop() });
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeCoord(i) {
|
function removeCoord(i) {
|
||||||
props.shape.coords.splice(i, 1);
|
const removed = props.shape.coords.splice(i, 1)[0];
|
||||||
props.onChange?.();
|
props.onChange?.({ undo: () => props.shape.coords.splice(i, 0, removed) });
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSvg() {
|
function renderSvg() {
|
||||||
@ -55,19 +60,14 @@ export const ShapeEditor = {
|
|||||||
return [sx, sy];
|
return [sx, sy];
|
||||||
};
|
};
|
||||||
|
|
||||||
const points = coords.map(c => toSvg(c).join(',') ).join(' ');
|
const points = coords.map(c => toSvg(c).join(',')).join(' ');
|
||||||
|
|
||||||
return h('svg', {
|
return h('svg', {
|
||||||
class: 'se-shape-svg',
|
class: 'se-shape-svg',
|
||||||
viewBox: `0 0 ${W} ${H}`,
|
viewBox: `0 0 ${W} ${H}`,
|
||||||
preserveAspectRatio: 'none',
|
preserveAspectRatio: 'none',
|
||||||
}, [
|
}, [
|
||||||
h('polyline', {
|
h('polyline', { points, fill: 'none', stroke: '#2a6aaa', 'stroke-width': '1.5' }),
|
||||||
points,
|
|
||||||
fill: 'none',
|
|
||||||
stroke: '#2a6aaa',
|
|
||||||
'stroke-width': '1.5',
|
|
||||||
}),
|
|
||||||
...coords.map(c => {
|
...coords.map(c => {
|
||||||
const [sx, sy] = toSvg(c);
|
const [sx, sy] = toSvg(c);
|
||||||
return h('circle', { cx: sx, cy: sy, r: 2.5, fill: '#6aacff' });
|
return h('circle', { cx: sx, cy: sy, r: 2.5, fill: '#6aacff' });
|
||||||
@ -101,7 +101,11 @@ export const ShapeEditor = {
|
|||||||
})),
|
})),
|
||||||
h('td', null, h('input', {
|
h('td', null, h('input', {
|
||||||
type: 'checkbox', checked: !!coord.isSharp,
|
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) }, '✕')),
|
h('td', null, h('button', { onClick: () => removeCoord(i) }, '✕')),
|
||||||
])
|
])
|
||||||
|
|||||||
@ -1,35 +1,20 @@
|
|||||||
import { h, ref, onMounted, onUnmounted } from 'vue';
|
import { h, onMounted, onUnmounted, ref } from 'vue';
|
||||||
import { fetchStatus } from '../api.js';
|
import { fetchStatus } from '../api.js';
|
||||||
|
|
||||||
export const StatusPoller = {
|
export const StatusPoller = {
|
||||||
props: ['store'],
|
props: ['store'],
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const timer = ref(null);
|
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() {
|
async function poll() {
|
||||||
try {
|
try {
|
||||||
const status = await fetchStatus(props.store.credentials);
|
const status = await fetchStatus(props.store.credentials);
|
||||||
if (status.eta) eta.value = status.eta;
|
|
||||||
props.store.synthesisStatus = status;
|
props.store.synthesisStatus = status;
|
||||||
if (status.frozen) {
|
if (status.frozen) return;
|
||||||
// done — stop polling
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// transient error — keep polling
|
// transient — keep polling
|
||||||
}
|
}
|
||||||
timer.value = setTimeout(poll, nextInterval(props.store.synthesisStatus));
|
timer.value = setTimeout(poll, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => { poll(); });
|
onMounted(() => { poll(); });
|
||||||
@ -39,10 +24,16 @@ export const StatusPoller = {
|
|||||||
const s = props.store.synthesisStatus;
|
const s = props.store.synthesisStatus;
|
||||||
if (!s) return h('div', { class: 'se-status' }, 'Polling…');
|
if (!s) return h('div', { class: 'se-status' }, 'Polling…');
|
||||||
|
|
||||||
const pct = s.progress ?? 0;
|
const total = s.notes_in_total ?? 0;
|
||||||
const label = s.frozen
|
const done = s.currently_rendered_notes ?? 0;
|
||||||
? (s.error ? `Error: ${s.error}` : 'Done')
|
const pct = total > 0 ? Math.min(100, Math.round(done / total * 100)) : 0;
|
||||||
: `${s.state ?? 'Running'} ${pct}%`;
|
|
||||||
|
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' }, [
|
return h('div', { class: 'se-status' }, [
|
||||||
h('span', null, label),
|
h('span', null, label),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user