feat(canvas): Add file browser, introspection, and improve node flow

Phase 1-7 of Canvas V4 Ralph Loop implementation:

Backend:
- Add /api/files routes for browsing model files
- Add /api/nx routes for NX model introspection
- Add NXIntrospector service to discover expressions and extractors
- Add health check with database status

Frontend:
- Add FileBrowser component for selecting .sim/.prt/.fem files
- Add IntrospectionPanel to discover expressions and extractors
- Update NodeConfigPanel with browse and introspect buttons
- Update schema with NODE_HANDLES for proper flow direction
- Update validation for correct DesignVar -> Model -> Solver flow
- Update useCanvasStore.addNode() to accept custom data

Flow correction: Design Variables now connect TO Model (as source),
not FROM Model. This matches the actual data flow in optimization.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-16 14:47:10 -05:00
parent 62284a995e
commit 1c7c7aff05
13 changed files with 4401 additions and 25 deletions

View File

@@ -89,14 +89,63 @@ export interface CanvasEdge {
targetHandle?: string;
}
// Valid connections
// Valid connections - defines what a node can connect TO (as source)
// Flow: DesignVar -> Model -> Solver -> Extractor -> Objective/Constraint -> Algorithm -> Surrogate
export const VALID_CONNECTIONS: Record<NodeType, NodeType[]> = {
model: ['solver', 'designVar'],
solver: ['extractor'],
designVar: ['model'],
extractor: ['objective', 'constraint'],
objective: ['algorithm'],
constraint: ['algorithm'],
algorithm: ['surrogate'],
surrogate: [],
model: ['solver'], // Model outputs to Solver
solver: ['extractor'], // Solver outputs to Extractor
designVar: ['model'], // DesignVar outputs to Model (expressions feed into model)
extractor: ['objective', 'constraint'], // Extractor outputs to Objective/Constraint
objective: ['algorithm'], // Objective outputs to Algorithm
constraint: ['algorithm'], // Constraint outputs to Algorithm
algorithm: ['surrogate'], // Algorithm outputs to Surrogate
surrogate: [], // Surrogate is terminal
};
// Node handle configuration for proper flow direction
export interface HandleConfig {
id: string;
label?: string;
}
export interface NodeHandleConfig {
inputs: HandleConfig[];
outputs: HandleConfig[];
}
// Define handles for each node type
// Flow: DesignVar(s) -> Model -> Solver -> Extractor(s) -> Objective(s) -> Algorithm
export const NODE_HANDLES: Record<NodeType, NodeHandleConfig> = {
model: {
inputs: [{ id: 'params', label: 'Parameters' }], // Receives from DesignVars
outputs: [{ id: 'sim', label: 'Simulation' }], // Sends to Solver
},
solver: {
inputs: [{ id: 'model', label: 'Model' }], // Receives from Model
outputs: [{ id: 'results', label: 'Results' }], // Sends to Extractors
},
designVar: {
inputs: [], // No inputs - this is a source
outputs: [{ id: 'value', label: 'Value' }], // Sends to Model
},
extractor: {
inputs: [{ id: 'results', label: 'Results' }], // Receives from Solver
outputs: [{ id: 'value', label: 'Value' }], // Sends to Objective/Constraint
},
objective: {
inputs: [{ id: 'value', label: 'Value' }], // Receives from Extractor
outputs: [{ id: 'objective', label: 'Objective' }], // Sends to Algorithm
},
constraint: {
inputs: [{ id: 'value', label: 'Value' }], // Receives from Extractor
outputs: [{ id: 'constraint', label: 'Constraint' }], // Sends to Algorithm
},
algorithm: {
inputs: [{ id: 'objectives', label: 'Objectives' }], // Receives from Objectives/Constraints
outputs: [{ id: 'algo', label: 'Algorithm' }], // Sends to Surrogate
},
surrogate: {
inputs: [{ id: 'algo', label: 'Algorithm' }], // Receives from Algorithm
outputs: [], // No outputs - this is a sink
},
};

View File

@@ -74,13 +74,68 @@ export function validateGraph(
}
}
// Check connectivity
// Check connectivity - verify proper flow direction
// Design Variables should connect TO Model (as source -> target)
const modelNodes = nodes.filter(n => n.data.type === 'model');
for (const dvar of designVars) {
const connectsToModel = edges.some(e =>
e.source === dvar.id && modelNodes.some(m => m.id === e.target)
);
if (!connectsToModel) {
warnings.push(`${dvar.data.label} is not connected to a Model`);
}
}
// Model should connect TO Solver
const solverNodes = nodes.filter(n => n.data.type === 'solver');
for (const model of modelNodes) {
const connectsToSolver = edges.some(e =>
e.source === model.id && solverNodes.some(s => s.id === e.target)
);
if (!connectsToSolver) {
errors.push(`${model.data.label} is not connected to a Solver`);
}
}
// Solver should connect TO Extractors
for (const solver of solverNodes) {
const connectsToExtractor = edges.some(e =>
e.source === solver.id && extractors.some(ex => ex.id === e.target)
);
if (!connectsToExtractor) {
warnings.push(`${solver.data.label} is not connected to any Extractor`);
}
}
// Extractors should connect TO Objectives or Constraints
const objectives = nodes.filter(n => n.data.type === 'objective');
const constraints = nodes.filter(n => n.data.type === 'constraint');
for (const extractor of extractors) {
const connectsToObjective = edges.some(e =>
e.source === extractor.id &&
(objectives.some(obj => obj.id === e.target) || constraints.some(c => c.id === e.target))
);
if (!connectsToObjective) {
warnings.push(`${extractor.data.label} is not connected to any Objective or Constraint`);
}
}
// Objectives should connect TO Algorithm
const algorithmNodes = nodes.filter(n => n.data.type === 'algorithm');
for (const obj of objectives) {
const hasIncoming = edges.some(e => e.target === obj.id);
const hasIncoming = edges.some(e =>
extractors.some(ex => ex.id === e.source) && e.target === obj.id
);
if (!hasIncoming) {
errors.push(`${obj.data.label} has no connected extractor`);
}
const connectsToAlgorithm = edges.some(e =>
e.source === obj.id && algorithmNodes.some(a => a.id === e.target)
);
if (!connectsToAlgorithm) {
warnings.push(`${obj.data.label} is not connected to an Algorithm`);
}
}
return {