feat(canvas): project edges from objective/constraint source
This commit is contained in:
@@ -422,6 +422,89 @@ function SpecRendererInner({
|
||||
}
|
||||
}, [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
|
||||
const nodes = useMemo(() => {
|
||||
const baseNodes = specToNodes(spec);
|
||||
|
||||
Reference in New Issue
Block a user