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

@@ -1,5 +1,9 @@
import { useState } from 'react';
import { FolderSearch, Microscope } from 'lucide-react';
import { useCanvasStore } from '../../../hooks/useCanvasStore';
import { ExpressionSelector } from './ExpressionSelector';
import { FileBrowser } from './FileBrowser';
import { IntrospectionPanel } from './IntrospectionPanel';
import {
ModelNodeData,
SolverNodeData,
@@ -24,6 +28,9 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
const { nodes, updateNodeData, deleteSelected } = useCanvasStore();
const node = nodes.find((n) => n.id === nodeId);
const [showFileBrowser, setShowFileBrowser] = useState(false);
const [showIntrospection, setShowIntrospection] = useState(false);
if (!node) return null;
const { data } = node;
@@ -63,15 +70,24 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
<>
<div>
<label className={labelClass}>
File Path
Model File
</label>
<input
type="text"
value={(data as ModelNodeData).filePath || ''}
onChange={(e) => handleChange('filePath', e.target.value)}
placeholder="path/to/model.prt"
className={`${inputClass} font-mono text-sm`}
/>
<div className="flex gap-2">
<input
type="text"
value={(data as ModelNodeData).filePath || ''}
onChange={(e) => handleChange('filePath', e.target.value)}
placeholder="path/to/model.sim"
className={`${inputClass} font-mono text-sm flex-1`}
/>
<button
onClick={() => setShowFileBrowser(true)}
className="px-3 py-2 bg-dark-700 hover:bg-dark-600 rounded-lg text-dark-300 hover:text-white transition-colors"
title="Browse files"
>
<FolderSearch size={18} />
</button>
</div>
</div>
<div>
<label className={labelClass}>
@@ -86,8 +102,21 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
<option value="prt">Part (.prt)</option>
<option value="fem">FEM (.fem)</option>
<option value="sim">Simulation (.sim)</option>
<option value="afem">Assembled FEM (.afem)</option>
</select>
</div>
{/* Introspect Button */}
{(data as ModelNodeData).filePath && (
<button
onClick={() => setShowIntrospection(true)}
className="w-full flex items-center justify-center gap-2 px-3 py-2.5 bg-primary-500/20
hover:bg-primary-500/30 border border-primary-500/30 rounded-lg
text-primary-400 text-sm font-medium transition-colors"
>
<Microscope size={16} />
Introspect Model
</button>
)}
</>
)}
@@ -385,6 +414,27 @@ export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
</>
)}
</div>
{/* File Browser Modal */}
<FileBrowser
isOpen={showFileBrowser}
onClose={() => setShowFileBrowser(false)}
onSelect={(path, fileType) => {
handleChange('filePath', path);
handleChange('fileType', fileType.replace('.', ''));
}}
fileTypes={['.sim', '.prt', '.fem', '.afem']}
/>
{/* Introspection Panel */}
{showIntrospection && (data as ModelNodeData).filePath && (
<div className="fixed top-20 right-96 z-40">
<IntrospectionPanel
filePath={(data as ModelNodeData).filePath!}
onClose={() => setShowIntrospection(false)}
/>
</div>
)}
</div>
);
}