feat(canvas): project edges from objective/constraint source

This commit is contained in:
2026-01-29 03:01:47 +00:00
parent 993c1ff17f
commit f47b390ed7

View File

@@ -422,6 +422,89 @@ function SpecRendererInner({
} }
}, [studyId, loadSpec, onStudyChange]); }, [studyId, loadSpec, onStudyChange]);
// -------------------------------------------------------------------------
// Option A: Edge projection sync (source fields are truth)
// Keep canvas edges in sync when user edits objective/constraint source in panels.
// We only enforce Extractor -> Objective/Constraint wiring edges here.
// -------------------------------------------------------------------------
const isEdgeSyncingRef = useRef(false);
useEffect(() => {
if (!spec || !studyId) return;
if (isEdgeSyncingRef.current) return;
const current = spec.canvas?.edges || [];
// Compute desired extractor->objective/constraint edges from source fields
const desiredPairs = new Set<string>();
for (const obj of spec.objectives || []) {
const extractorId = obj.source?.extractor_id;
const outputName = obj.source?.output_name;
if (extractorId && outputName && extractorId !== '__UNSET__' && outputName !== '__UNSET__') {
desiredPairs.add(`${extractorId}__${obj.id}`);
}
}
for (const con of spec.constraints || []) {
const extractorId = con.source?.extractor_id;
const outputName = con.source?.output_name;
if (extractorId && outputName && extractorId !== '__UNSET__' && outputName !== '__UNSET__') {
desiredPairs.add(`${extractorId}__${con.id}`);
}
}
// Identify current wiring edges (ext_* -> obj_*/con_*)
const currentWiringPairs = new Set<string>();
for (const e of current) {
if (e.source?.startsWith('ext_') && (e.target?.startsWith('obj_') || e.target?.startsWith('con_'))) {
currentWiringPairs.add(`${e.source}__${e.target}`);
}
}
// Determine adds/removes
const toAdd: Array<{ source: string; target: string }> = [];
for (const key of desiredPairs) {
if (!currentWiringPairs.has(key)) {
const [source, target] = key.split('__');
toAdd.push({ source, target });
}
}
const toRemove: Array<{ source: string; target: string }> = [];
for (const key of currentWiringPairs) {
if (!desiredPairs.has(key)) {
const [source, target] = key.split('__');
toRemove.push({ source, target });
}
}
if (toAdd.length === 0 && toRemove.length === 0) return;
isEdgeSyncingRef.current = true;
(async () => {
try {
// Remove stale edges first
for (const e of toRemove) {
await removeEdge(e.source, e.target);
}
// Add missing edges
for (const e of toAdd) {
await addEdge(e.source, e.target);
}
} catch (err) {
console.error('[SpecRenderer] Edge projection sync failed:', err);
} finally {
// Small delay avoids re-entrancy storms when backend broadcasts updates
setTimeout(() => {
isEdgeSyncingRef.current = false;
}, 250);
}
})();
}, [spec, studyId, addEdge, removeEdge]);
// Convert spec to ReactFlow nodes // Convert spec to ReactFlow nodes
const nodes = useMemo(() => { const nodes = useMemo(() => {
const baseNodes = specToNodes(spec); const baseNodes = specToNodes(spec);