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,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,
|
||||
};
|
||||
Reference in New Issue
Block a user