feat(canvas): sync objective/constraint source on edge connect/delete
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user