Files
Atomizer/atomizer-dashboard/frontend/src/components/canvas/panels/NodeConfigPanel.tsx
Anto01 7919511bb2 feat: Phase 1 - Canvas with React Flow
- 8 node types (Model, Solver, DesignVar, Extractor, Objective, Constraint, Algorithm, Surrogate)
- Drag-drop from palette to canvas
- Node configuration panels
- Graph validation with error/warning display
- Intent JSON serialization
- Zustand state management

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 20:00:35 -05:00

373 lines
14 KiB
TypeScript

import { useCanvasStore } from '../../../hooks/useCanvasStore';
import {
ModelNodeData,
SolverNodeData,
DesignVarNodeData,
AlgorithmNodeData,
ObjectiveNodeData,
ExtractorNodeData,
ConstraintNodeData,
SurrogateNodeData
} from '../../../lib/canvas/schema';
interface NodeConfigPanelProps {
nodeId: string;
}
export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
const { nodes, updateNodeData, deleteSelected } = useCanvasStore();
const node = nodes.find((n) => n.id === nodeId);
if (!node) return null;
const { data } = node;
const handleChange = (field: string, value: unknown) => {
updateNodeData(nodeId, { [field]: value, configured: true });
};
return (
<div className="w-80 bg-white border-l border-gray-200 p-4 overflow-y-auto">
<div className="flex justify-between items-center mb-4">
<h3 className="font-semibold text-gray-800">Configure {data.label}</h3>
<button
onClick={deleteSelected}
className="text-red-500 hover:text-red-700 text-sm"
>
Delete
</button>
</div>
<div className="space-y-4">
{/* Common: Label */}
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
Label
</label>
<input
type="text"
value={data.label}
onChange={(e) => handleChange('label', e.target.value)}
className="w-full px-3 py-2 border rounded-lg"
/>
</div>
{/* Type-specific fields */}
{data.type === 'model' && (
<>
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
File Path
</label>
<input
type="text"
value={(data as ModelNodeData).filePath || ''}
onChange={(e) => handleChange('filePath', e.target.value)}
placeholder="path/to/model.prt"
className="w-full px-3 py-2 border rounded-lg font-mono text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
File Type
</label>
<select
value={(data as ModelNodeData).fileType || ''}
onChange={(e) => handleChange('fileType', e.target.value)}
className="w-full px-3 py-2 border rounded-lg"
>
<option value="">Select...</option>
<option value="prt">Part (.prt)</option>
<option value="fem">FEM (.fem)</option>
<option value="sim">Simulation (.sim)</option>
</select>
</div>
</>
)}
{data.type === 'solver' && (
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
Solution Type
</label>
<select
value={(data as SolverNodeData).solverType || ''}
onChange={(e) => handleChange('solverType', e.target.value)}
className="w-full px-3 py-2 border rounded-lg"
>
<option value="">Select...</option>
<option value="SOL101">SOL 101 - Linear Static</option>
<option value="SOL103">SOL 103 - Modal Analysis</option>
<option value="SOL105">SOL 105 - Buckling</option>
<option value="SOL106">SOL 106 - Nonlinear Static</option>
<option value="SOL111">SOL 111 - Frequency Response</option>
<option value="SOL112">SOL 112 - Transient Response</option>
</select>
</div>
)}
{data.type === 'designVar' && (
<>
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
Expression Name
</label>
<input
type="text"
value={(data as DesignVarNodeData).expressionName || ''}
onChange={(e) => handleChange('expressionName', e.target.value)}
placeholder="thickness"
className="w-full px-3 py-2 border rounded-lg font-mono"
/>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
Min
</label>
<input
type="number"
value={(data as DesignVarNodeData).minValue ?? ''}
onChange={(e) => handleChange('minValue', parseFloat(e.target.value))}
className="w-full px-3 py-2 border rounded-lg"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
Max
</label>
<input
type="number"
value={(data as DesignVarNodeData).maxValue ?? ''}
onChange={(e) => handleChange('maxValue', parseFloat(e.target.value))}
className="w-full px-3 py-2 border rounded-lg"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
Unit
</label>
<input
type="text"
value={(data as DesignVarNodeData).unit || ''}
onChange={(e) => handleChange('unit', e.target.value)}
placeholder="mm"
className="w-full px-3 py-2 border rounded-lg"
/>
</div>
</>
)}
{data.type === 'extractor' && (
<>
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
Extractor ID
</label>
<select
value={(data as ExtractorNodeData).extractorId || ''}
onChange={(e) => {
const id = e.target.value;
const names: Record<string, string> = {
'E1': 'Displacement',
'E2': 'Frequency',
'E3': 'Solid Stress',
'E4': 'BDF Mass',
'E5': 'CAD Mass',
'E8': 'Zernike (OP2)',
'E9': 'Zernike (CSV)',
'E10': 'Zernike (RMS)',
};
handleChange('extractorId', id);
handleChange('extractorName', names[id] || id);
}}
className="w-full px-3 py-2 border rounded-lg"
>
<option value="">Select...</option>
<option value="E1">E1 - Displacement</option>
<option value="E2">E2 - Frequency</option>
<option value="E3">E3 - Solid Stress</option>
<option value="E4">E4 - BDF Mass</option>
<option value="E5">E5 - CAD Mass</option>
<option value="E8">E8 - Zernike (OP2)</option>
<option value="E9">E9 - Zernike (CSV)</option>
<option value="E10">E10 - Zernike (RMS)</option>
</select>
</div>
</>
)}
{data.type === 'algorithm' && (
<>
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
Method
</label>
<select
value={(data as AlgorithmNodeData).method || ''}
onChange={(e) => handleChange('method', e.target.value)}
className="w-full px-3 py-2 border rounded-lg"
>
<option value="">Select...</option>
<option value="TPE">TPE (Tree Parzen Estimator)</option>
<option value="CMA-ES">CMA-ES (Evolution Strategy)</option>
<option value="NSGA-II">NSGA-II (Multi-Objective)</option>
<option value="GP-BO">GP-BO (Gaussian Process)</option>
<option value="RandomSearch">Random Search</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
Max Trials
</label>
<input
type="number"
value={(data as AlgorithmNodeData).maxTrials ?? ''}
onChange={(e) => handleChange('maxTrials', parseInt(e.target.value))}
placeholder="100"
className="w-full px-3 py-2 border rounded-lg"
/>
</div>
</>
)}
{data.type === 'objective' && (
<>
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
Objective Name
</label>
<input
type="text"
value={(data as ObjectiveNodeData).name || ''}
onChange={(e) => handleChange('name', e.target.value)}
placeholder="mass"
className="w-full px-3 py-2 border rounded-lg"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
Direction
</label>
<select
value={(data as ObjectiveNodeData).direction || 'minimize'}
onChange={(e) => handleChange('direction', e.target.value)}
className="w-full px-3 py-2 border rounded-lg"
>
<option value="minimize">Minimize</option>
<option value="maximize">Maximize</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
Weight
</label>
<input
type="number"
step="0.1"
value={(data as ObjectiveNodeData).weight ?? 1}
onChange={(e) => handleChange('weight', parseFloat(e.target.value))}
className="w-full px-3 py-2 border rounded-lg"
/>
</div>
</>
)}
{data.type === 'constraint' && (
<>
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
Constraint Name
</label>
<input
type="text"
value={(data as ConstraintNodeData).name || ''}
onChange={(e) => handleChange('name', e.target.value)}
placeholder="max_stress"
className="w-full px-3 py-2 border rounded-lg"
/>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
Operator
</label>
<select
value={(data as ConstraintNodeData).operator || '<='}
onChange={(e) => handleChange('operator', e.target.value)}
className="w-full px-3 py-2 border rounded-lg"
>
<option value="<">&lt;</option>
<option value="<=">&lt;=</option>
<option value=">">&gt;</option>
<option value=">=">&gt;=</option>
<option value="==">==</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
Value
</label>
<input
type="number"
value={(data as ConstraintNodeData).value ?? ''}
onChange={(e) => handleChange('value', parseFloat(e.target.value))}
className="w-full px-3 py-2 border rounded-lg"
/>
</div>
</div>
</>
)}
{data.type === 'surrogate' && (
<>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="surrogate-enabled"
checked={(data as SurrogateNodeData).enabled || false}
onChange={(e) => handleChange('enabled', e.target.checked)}
className="w-4 h-4"
/>
<label htmlFor="surrogate-enabled" className="text-sm font-medium text-gray-600">
Enable Neural Surrogate
</label>
</div>
{(data as SurrogateNodeData).enabled && (
<>
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
Model Type
</label>
<select
value={(data as SurrogateNodeData).modelType || ''}
onChange={(e) => handleChange('modelType', e.target.value)}
className="w-full px-3 py-2 border rounded-lg"
>
<option value="">Select...</option>
<option value="MLP">MLP (Multi-Layer Perceptron)</option>
<option value="GNN">GNN (Graph Neural Network)</option>
<option value="Ensemble">Ensemble</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-600 mb-1">
Min Trials Before Activation
</label>
<input
type="number"
value={(data as SurrogateNodeData).minTrials ?? 20}
onChange={(e) => handleChange('minTrials', parseInt(e.target.value))}
className="w-full px-3 py-2 border rounded-lg"
/>
</div>
</>
)}
</>
)}
</div>
</div>
);
}