From 00dd88599ec80ce97b66c3800609317177c7d11c Mon Sep 17 00:00:00 2001 From: Antoine Date: Thu, 29 Jan 2026 02:39:45 +0000 Subject: [PATCH] feat(canvas): sync objective/constraint source on edge connect/delete --- .../src/components/canvas/SpecRenderer.tsx | 93 +++++++++++++++++-- 1 file changed, 83 insertions(+), 10 deletions(-) diff --git a/atomizer-dashboard/frontend/src/components/canvas/SpecRenderer.tsx b/atomizer-dashboard/frontend/src/components/canvas/SpecRenderer.tsx index 3bbfc1d7..7370e852 100644 --- a/atomizer-dashboard/frontend/src/components/canvas/SpecRenderer.tsx +++ b/atomizer-dashboard/frontend/src/components/canvas/SpecRenderer.tsx @@ -235,6 +235,7 @@ function SpecRendererInner({ clearSelection, updateNodePosition, addNode, + updateNode, addEdge, removeEdge, removeNode, @@ -521,34 +522,106 @@ function SpecRendererInner({ (changes: EdgeChange[]) => { if (!editable) return; + const classify = (id: string): string => { + if (id === 'model' || id === 'solver' || id === 'algorithm' || id === 'surrogate') return id; + const prefix = id.split('_')[0]; + if (prefix === 'dv') return 'designVar'; + if (prefix === 'ext') return 'extractor'; + if (prefix === 'obj') return 'objective'; + if (prefix === 'con') return 'constraint'; + return 'unknown'; + }; + for (const change of changes) { if (change.type === 'remove') { // Find the edge being removed const edge = edges.find((e) => e.id === change.id); - if (edge) { - removeEdge(edge.source, edge.target).catch((err) => { - console.error('Failed to remove edge:', err); + if (!edge) continue; + + const sourceType = classify(edge.source); + const targetType = classify(edge.target); + + // First remove the visual edge + removeEdge(edge.source, edge.target).catch((err) => { + console.error('Failed to remove edge:', err); + setError(err.message); + }); + + // Option A truth model: if we removed Extractor -> Objective/Constraint, + // clear the target's source to avoid stale runnable config. + if (sourceType === 'extractor' && (targetType === 'objective' || targetType === 'constraint')) { + updateNode(edge.target, { + // Setting to an empty object would violate schema; clear to placeholder + // and let validation catch missing wiring. + source: { extractor_id: '', output_name: '' }, + }).catch((err) => { + console.error('Failed to clear source on node:', err); setError(err.message); }); } } } }, - [editable, edges, removeEdge, setError] + [editable, edges, removeEdge, setError, updateNode] ); // Handle new connections const onConnect = useCallback( - (connection: Connection) => { + async (connection: Connection) => { if (!editable) return; if (!connection.source || !connection.target) return; - addEdge(connection.source, connection.target).catch((err) => { - console.error('Failed to add edge:', err); - setError(err.message); - }); + const sourceId = connection.source; + const targetId = connection.target; + + // Helper: classify nodes by ID (synthetic vs spec-backed) + const classify = (id: string): string => { + if (id === 'model' || id === 'solver' || id === 'algorithm' || id === 'surrogate') return id; + const prefix = id.split('_')[0]; + if (prefix === 'dv') return 'designVar'; + if (prefix === 'ext') return 'extractor'; + if (prefix === 'obj') return 'objective'; + if (prefix === 'con') return 'constraint'; + return 'unknown'; + }; + + const sourceType = classify(sourceId); + const targetType = classify(targetId); + + try { + // Always persist the visual edge (for now) + await addEdge(sourceId, targetId); + + // Option A truth model: objective/constraint source is the real linkage. + // When user connects Extractor -> Objective/Constraint, update *.source accordingly. + if (spec && sourceType === 'extractor' && (targetType === 'objective' || targetType === 'constraint')) { + const ext = spec.extractors.find((e) => e.id === sourceId); + + // Choose a sensible default output: + // - prefer 'value' if present + // - else if only one output, use it + // - else use first output + const outputs = ext?.outputs || []; + const preferred = outputs.find((o) => o.name === 'value')?.name; + const outputName = + preferred || (outputs.length === 1 ? outputs[0].name : outputs.length > 0 ? outputs[0].name : 'value'); + + if (targetType === 'objective') { + await updateNode(targetId, { + source: { extractor_id: sourceId, output_name: outputName }, + }); + } else { + await updateNode(targetId, { + source: { extractor_id: sourceId, output_name: outputName }, + }); + } + } + } catch (err) { + console.error('Failed to add connection:', err); + setError(err instanceof Error ? err.message : 'Failed to add connection'); + } }, - [editable, addEdge, setError] + [editable, addEdge, setError, spec, updateNode] ); // Handle node clicks for selection