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>
This commit is contained in:
2026-01-14 20:00:35 -05:00
parent 73a7b9d9f1
commit 7919511bb2
24 changed files with 1915 additions and 6 deletions

View File

@@ -0,0 +1,146 @@
import { useCallback, useRef, DragEvent } from 'react';
import ReactFlow, {
Background,
Controls,
MiniMap,
ReactFlowProvider,
ReactFlowInstance,
} from 'reactflow';
import 'reactflow/dist/style.css';
import { nodeTypes } from './nodes';
import { NodePalette } from './palette/NodePalette';
import { NodeConfigPanel } from './panels/NodeConfigPanel';
import { ValidationPanel } from './panels/ValidationPanel';
import { useCanvasStore } from '../../hooks/useCanvasStore';
import { NodeType } from '../../lib/canvas/schema';
function CanvasFlow() {
const reactFlowWrapper = useRef<HTMLDivElement>(null);
const reactFlowInstance = useRef<ReactFlowInstance | null>(null);
const {
nodes,
edges,
selectedNode,
onNodesChange,
onEdgesChange,
onConnect,
addNode,
selectNode,
validation,
validate,
toIntent,
} = useCanvasStore();
const onDragOver = useCallback((event: DragEvent) => {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
}, []);
const onDrop = useCallback(
(event: DragEvent) => {
event.preventDefault();
const type = event.dataTransfer.getData('application/reactflow') as NodeType;
if (!type || !reactFlowInstance.current || !reactFlowWrapper.current) return;
const bounds = reactFlowWrapper.current.getBoundingClientRect();
const position = reactFlowInstance.current.screenToFlowPosition({
x: event.clientX - bounds.left,
y: event.clientY - bounds.top,
});
addNode(type, position);
},
[addNode]
);
const onNodeClick = useCallback(
(_: React.MouseEvent, node: { id: string }) => {
selectNode(node.id);
},
[selectNode]
);
const onPaneClick = useCallback(() => {
selectNode(null);
}, [selectNode]);
const handleExecute = () => {
const result = validate();
if (result.valid) {
const intent = toIntent();
// Send to chat
console.log('Executing intent:', JSON.stringify(intent, null, 2));
// TODO: Connect to useChat hook
alert('Intent generated! Check console for JSON output.\n\nIn Phase 2, this will be sent to Claude.');
}
};
return (
<div className="flex h-full">
{/* Left: Node Palette */}
<NodePalette />
{/* Center: Canvas */}
<div className="flex-1 relative" ref={reactFlowWrapper}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onInit={(instance) => { reactFlowInstance.current = instance; }}
onDragOver={onDragOver}
onDrop={onDrop}
onNodeClick={onNodeClick}
onPaneClick={onPaneClick}
nodeTypes={nodeTypes}
fitView
>
<Background />
<Controls />
<MiniMap />
</ReactFlow>
{/* Execute Button */}
<div className="absolute bottom-4 right-4 flex gap-2 z-10">
<button
onClick={validate}
className="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors"
>
Validate
</button>
<button
onClick={handleExecute}
disabled={!validation.valid}
className={`px-4 py-2 rounded-lg transition-colors ${
validation.valid
? 'bg-blue-600 text-white hover:bg-blue-700'
: 'bg-gray-300 text-gray-500 cursor-not-allowed'
}`}
>
Execute with Claude
</button>
</div>
{/* Validation Messages */}
{(validation.errors.length > 0 || validation.warnings.length > 0) && (
<ValidationPanel validation={validation} />
)}
</div>
{/* Right: Config Panel */}
{selectedNode && <NodeConfigPanel nodeId={selectedNode} />}
</div>
);
}
export function AtomizerCanvas() {
return (
<ReactFlowProvider>
<CanvasFlow />
</ReactFlowProvider>
);
}

View File

@@ -0,0 +1,5 @@
export { AtomizerCanvas } from './AtomizerCanvas';
export { NodePalette } from './palette/NodePalette';
export { NodeConfigPanel } from './panels/NodeConfigPanel';
export { ValidationPanel } from './panels/ValidationPanel';
export * from './nodes';

View File

@@ -0,0 +1,17 @@
import { memo } from 'react';
import { NodeProps } from 'reactflow';
import { BaseNode } from './BaseNode';
import { AlgorithmNodeData } from '../../../lib/canvas/schema';
function AlgorithmNodeComponent(props: NodeProps<AlgorithmNodeData>) {
const { data } = props;
return (
<BaseNode {...props} icon={<span>🧠</span>} color="text-indigo-600">
{data.method && <div>{data.method}</div>}
{data.maxTrials && (
<div className="text-xs text-gray-400">{data.maxTrials} trials</div>
)}
</BaseNode>
);
}
export const AlgorithmNode = memo(AlgorithmNodeComponent);

View File

@@ -0,0 +1,72 @@
import { memo, ReactNode } from 'react';
import { Handle, Position, NodeProps } from 'reactflow';
import { BaseNodeData } from '../../../lib/canvas/schema';
interface BaseNodeProps extends NodeProps<BaseNodeData> {
icon: ReactNode;
color: string;
children?: ReactNode;
inputs?: number;
outputs?: number;
}
function BaseNodeComponent({
data,
selected,
icon,
color,
children,
inputs = 1,
outputs = 1,
}: BaseNodeProps) {
return (
<div
className={`
px-4 py-3 rounded-lg border-2 min-w-[180px] bg-white shadow-sm
transition-all duration-200
${selected ? 'border-blue-500 shadow-lg' : 'border-gray-200'}
${!data.configured ? 'border-dashed' : ''}
${data.errors?.length ? 'border-red-400' : ''}
`}
>
{/* Input handles */}
{inputs > 0 && (
<Handle
type="target"
position={Position.Left}
className="w-3 h-3 !bg-gray-400"
/>
)}
{/* Header */}
<div className="flex items-center gap-2 mb-2">
<span className={`text-lg ${color}`}>{icon}</span>
<span className="font-medium text-gray-800">{data.label}</span>
{!data.configured && (
<span className="text-xs text-orange-500">!</span>
)}
</div>
{/* Content */}
{children && <div className="text-sm text-gray-600">{children}</div>}
{/* Errors */}
{data.errors?.length ? (
<div className="mt-2 text-xs text-red-500">
{data.errors[0]}
</div>
) : null}
{/* Output handles */}
{outputs > 0 && (
<Handle
type="source"
position={Position.Right}
className="w-3 h-3 !bg-gray-400"
/>
)}
</div>
);
}
export const BaseNode = memo(BaseNodeComponent);

View File

@@ -0,0 +1,19 @@
import { memo } from 'react';
import { NodeProps } from 'reactflow';
import { BaseNode } from './BaseNode';
import { ConstraintNodeData } from '../../../lib/canvas/schema';
function ConstraintNodeComponent(props: NodeProps<ConstraintNodeData>) {
const { data } = props;
return (
<BaseNode {...props} icon={<span>🚧</span>} color="text-orange-600">
{data.name && <div>{data.name}</div>}
{data.operator && data.value !== undefined && (
<div className="text-xs text-gray-400">
{data.operator} {data.value}
</div>
)}
</BaseNode>
);
}
export const ConstraintNode = memo(ConstraintNodeComponent);

View File

@@ -0,0 +1,19 @@
import { memo } from 'react';
import { NodeProps } from 'reactflow';
import { BaseNode } from './BaseNode';
import { DesignVarNodeData } from '../../../lib/canvas/schema';
function DesignVarNodeComponent(props: NodeProps<DesignVarNodeData>) {
const { data } = props;
return (
<BaseNode {...props} icon={<span>📐</span>} color="text-green-600">
{data.expressionName && <div className="font-mono">{data.expressionName}</div>}
{data.minValue !== undefined && data.maxValue !== undefined && (
<div className="text-xs text-gray-400">
[{data.minValue} - {data.maxValue}] {data.unit || ''}
</div>
)}
</BaseNode>
);
}
export const DesignVarNode = memo(DesignVarNodeComponent);

View File

@@ -0,0 +1,17 @@
import { memo } from 'react';
import { NodeProps } from 'reactflow';
import { BaseNode } from './BaseNode';
import { ExtractorNodeData } from '../../../lib/canvas/schema';
function ExtractorNodeComponent(props: NodeProps<ExtractorNodeData>) {
const { data } = props;
return (
<BaseNode {...props} icon={<span>🔬</span>} color="text-cyan-600">
{data.extractorName && <div>{data.extractorName}</div>}
{data.extractorId && (
<div className="text-xs text-gray-400">{data.extractorId}</div>
)}
</BaseNode>
);
}
export const ExtractorNode = memo(ExtractorNodeComponent);

View File

@@ -0,0 +1,19 @@
import { memo } from 'react';
import { NodeProps } from 'reactflow';
import { BaseNode } from './BaseNode';
import { ModelNodeData } from '../../../lib/canvas/schema';
function ModelNodeComponent(props: NodeProps<ModelNodeData>) {
const { data } = props;
return (
<BaseNode {...props} icon={<span>📦</span>} color="text-blue-600" inputs={0}>
{data.filePath && (
<div className="truncate max-w-[150px]">{data.filePath.split('/').pop()}</div>
)}
{data.fileType && (
<div className="text-xs text-gray-400">{data.fileType.toUpperCase()}</div>
)}
</BaseNode>
);
}
export const ModelNode = memo(ModelNodeComponent);

View File

@@ -0,0 +1,20 @@
import { memo } from 'react';
import { NodeProps } from 'reactflow';
import { BaseNode } from './BaseNode';
import { ObjectiveNodeData } from '../../../lib/canvas/schema';
function ObjectiveNodeComponent(props: NodeProps<ObjectiveNodeData>) {
const { data } = props;
return (
<BaseNode {...props} icon={<span>🎯</span>} color="text-red-600">
{data.name && <div>{data.name}</div>}
{data.direction && (
<div className="text-xs text-gray-400">
{data.direction === 'minimize' ? '↓ Minimize' : '↑ Maximize'}
{data.weight !== 1 && ` (w=${data.weight})`}
</div>
)}
</BaseNode>
);
}
export const ObjectiveNode = memo(ObjectiveNodeComponent);

View File

@@ -0,0 +1,14 @@
import { memo } from 'react';
import { NodeProps } from 'reactflow';
import { BaseNode } from './BaseNode';
import { SolverNodeData } from '../../../lib/canvas/schema';
function SolverNodeComponent(props: NodeProps<SolverNodeData>) {
const { data } = props;
return (
<BaseNode {...props} icon={<span></span>} color="text-purple-600">
{data.solverType && <div>{data.solverType}</div>}
</BaseNode>
);
}
export const SolverNode = memo(SolverNodeComponent);

View File

@@ -0,0 +1,17 @@
import { memo } from 'react';
import { NodeProps } from 'reactflow';
import { BaseNode } from './BaseNode';
import { SurrogateNodeData } from '../../../lib/canvas/schema';
function SurrogateNodeComponent(props: NodeProps<SurrogateNodeData>) {
const { data } = props;
return (
<BaseNode {...props} icon={<span>🚀</span>} color="text-pink-600" outputs={0}>
<div>{data.enabled ? 'Enabled' : 'Disabled'}</div>
{data.enabled && data.modelType && (
<div className="text-xs text-gray-400">{data.modelType}</div>
)}
</BaseNode>
);
}
export const SurrogateNode = memo(SurrogateNodeComponent);

View File

@@ -0,0 +1,30 @@
import { ModelNode } from './ModelNode';
import { SolverNode } from './SolverNode';
import { DesignVarNode } from './DesignVarNode';
import { ExtractorNode } from './ExtractorNode';
import { ObjectiveNode } from './ObjectiveNode';
import { ConstraintNode } from './ConstraintNode';
import { AlgorithmNode } from './AlgorithmNode';
import { SurrogateNode } from './SurrogateNode';
export {
ModelNode,
SolverNode,
DesignVarNode,
ExtractorNode,
ObjectiveNode,
ConstraintNode,
AlgorithmNode,
SurrogateNode,
};
export const nodeTypes = {
model: ModelNode,
solver: SolverNode,
designVar: DesignVarNode,
extractor: ExtractorNode,
objective: ObjectiveNode,
constraint: ConstraintNode,
algorithm: AlgorithmNode,
surrogate: SurrogateNode,
};

View File

@@ -0,0 +1,52 @@
import { DragEvent } from 'react';
import { NodeType } from '../../../lib/canvas/schema';
interface PaletteItem {
type: NodeType;
label: string;
icon: string;
description: string;
}
const PALETTE_ITEMS: PaletteItem[] = [
{ type: 'model', label: 'Model', icon: '📦', description: 'NX model file' },
{ type: 'solver', label: 'Solver', icon: '⚙️', description: 'Nastran solution' },
{ type: 'designVar', label: 'Design Variable', icon: '📐', description: 'Parameter to vary' },
{ type: 'extractor', label: 'Extractor', icon: '🔬', description: 'Physics extraction' },
{ type: 'objective', label: 'Objective', icon: '🎯', description: 'Optimization goal' },
{ type: 'constraint', label: 'Constraint', icon: '🚧', description: 'Limit condition' },
{ type: 'algorithm', label: 'Algorithm', icon: '🧠', description: 'Optimization method' },
{ type: 'surrogate', label: 'Surrogate', icon: '🚀', description: 'Neural acceleration' },
];
export function NodePalette() {
const onDragStart = (event: DragEvent, nodeType: NodeType) => {
event.dataTransfer.setData('application/reactflow', nodeType);
event.dataTransfer.effectAllowed = 'move';
};
return (
<div className="w-64 bg-gray-50 border-r border-gray-200 p-4 overflow-y-auto">
<h3 className="text-sm font-semibold text-gray-500 uppercase mb-4">
Components
</h3>
<div className="space-y-2">
{PALETTE_ITEMS.map((item) => (
<div
key={item.type}
draggable
onDragStart={(e) => onDragStart(e, item.type)}
className="flex items-center gap-3 p-3 bg-white rounded-lg border border-gray-200
cursor-grab hover:border-blue-300 hover:shadow-sm transition-all"
>
<span className="text-xl">{item.icon}</span>
<div>
<div className="font-medium text-gray-800">{item.label}</div>
<div className="text-xs text-gray-500">{item.description}</div>
</div>
</div>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,372 @@
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>
);
}

View File

@@ -0,0 +1,32 @@
import { ValidationResult } from '../../../lib/canvas/validation';
interface ValidationPanelProps {
validation: ValidationResult;
}
export function ValidationPanel({ validation }: ValidationPanelProps) {
return (
<div className="absolute top-4 left-1/2 transform -translate-x-1/2 max-w-md w-full z-10">
{validation.errors.length > 0 && (
<div className="bg-red-50 border border-red-200 rounded-lg p-3 mb-2">
<div className="font-medium text-red-800 mb-1">Errors</div>
<ul className="text-sm text-red-600 list-disc list-inside">
{validation.errors.map((error, i) => (
<li key={i}>{error}</li>
))}
</ul>
</div>
)}
{validation.warnings.length > 0 && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
<div className="font-medium text-yellow-800 mb-1">Warnings</div>
<ul className="text-sm text-yellow-600 list-disc list-inside">
{validation.warnings.map((warning, i) => (
<li key={i}>{warning}</li>
))}
</ul>
</div>
)}
</div>
);
}

View File

@@ -91,7 +91,7 @@ function getAvailableParams(trials: Trial[]): string[] {
export function NivoParallelCoordinates({
trials,
objectives,
objectives: _objectives,
designVariables,
paretoFront = [],
height = 400