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:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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="<"><</option>
|
||||
<option value="<="><=</option>
|
||||
<option value=">">></option>
|
||||
<option value=">=">>=</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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -91,7 +91,7 @@ function getAvailableParams(trials: Trial[]): string[] {
|
||||
|
||||
export function NivoParallelCoordinates({
|
||||
trials,
|
||||
objectives,
|
||||
objectives: _objectives,
|
||||
designVariables,
|
||||
paretoFront = [],
|
||||
height = 400
|
||||
|
||||
Reference in New Issue
Block a user