feat(canvas): sync objective/constraint source on edge connect/delete
This commit is contained in:
@@ -235,6 +235,7 @@ function SpecRendererInner({
|
|||||||
clearSelection,
|
clearSelection,
|
||||||
updateNodePosition,
|
updateNodePosition,
|
||||||
addNode,
|
addNode,
|
||||||
|
updateNode,
|
||||||
addEdge,
|
addEdge,
|
||||||
removeEdge,
|
removeEdge,
|
||||||
removeNode,
|
removeNode,
|
||||||
@@ -521,34 +522,106 @@ function SpecRendererInner({
|
|||||||
(changes: EdgeChange[]) => {
|
(changes: EdgeChange[]) => {
|
||||||
if (!editable) return;
|
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) {
|
for (const change of changes) {
|
||||||
if (change.type === 'remove') {
|
if (change.type === 'remove') {
|
||||||
// Find the edge being removed
|
// Find the edge being removed
|
||||||
const edge = edges.find((e) => e.id === change.id);
|
const edge = edges.find((e) => e.id === change.id);
|
||||||
if (edge) {
|
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) => {
|
removeEdge(edge.source, edge.target).catch((err) => {
|
||||||
console.error('Failed to remove edge:', err);
|
console.error('Failed to remove edge:', err);
|
||||||
setError(err.message);
|
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
|
// Handle new connections
|
||||||
const onConnect = useCallback(
|
const onConnect = useCallback(
|
||||||
(connection: Connection) => {
|
async (connection: Connection) => {
|
||||||
if (!editable) return;
|
if (!editable) return;
|
||||||
if (!connection.source || !connection.target) return;
|
if (!connection.source || !connection.target) return;
|
||||||
|
|
||||||
addEdge(connection.source, connection.target).catch((err) => {
|
const sourceId = connection.source;
|
||||||
console.error('Failed to add edge:', err);
|
const targetId = connection.target;
|
||||||
setError(err.message);
|
|
||||||
|
// 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
|
// Handle node clicks for selection
|
||||||
|
|||||||
Reference in New Issue
Block a user