feat(canvas): Add Phase 3+4 - Bidirectional sync, templates, and visual polish

Phase 3 - Bidirectional Sync:
- Add loadFromIntent and loadFromConfig to canvas store
- Create useIntentParser hook for parsing Claude messages
- Create ConfigImporter component (file upload, paste JSON, load study)
- Add import/clear buttons to CanvasView header

Phase 4 - Templates & Polish:
- Create template library with 5 presets:
  - Mass Minimization (single-objective)
  - Multi-Objective Pareto (NSGA-II)
  - Turbo Mode (with MLP surrogate)
  - Mirror Zernike (optical optimization)
  - Frequency Optimization (modal)
- Create TemplateSelector component with category filters
- Enhanced BaseNode with animations, glow effects, status indicators
- Add colorBg to all node types for visual distinction
- Add notification toast system
- Update all exports

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-14 20:30:28 -05:00
parent 1ae35382da
commit 5bd142780f
18 changed files with 1389 additions and 35 deletions

View File

@@ -6,7 +6,7 @@ import { AlgorithmNodeData } from '../../../lib/canvas/schema';
function AlgorithmNodeComponent(props: NodeProps<AlgorithmNodeData>) {
const { data } = props;
return (
<BaseNode {...props} icon={<span>🧠</span>} color="text-indigo-600">
<BaseNode {...props} icon={<span>🧠</span>} color="text-indigo-600" colorBg="bg-indigo-50">
{data.method && <div>{data.method}</div>}
{data.maxTrials && (
<div className="text-xs text-gray-400">{data.maxTrials} trials</div>

View File

@@ -5,6 +5,7 @@ import { BaseNodeData } from '../../../lib/canvas/schema';
interface BaseNodeProps extends NodeProps<BaseNodeData> {
icon: ReactNode;
color: string;
colorBg?: string;
children?: ReactNode;
inputs?: number;
outputs?: number;
@@ -15,56 +16,126 @@ function BaseNodeComponent({
selected,
icon,
color,
colorBg = 'bg-gray-50',
children,
inputs = 1,
outputs = 1,
}: BaseNodeProps) {
const hasError = data.errors && data.errors.length > 0;
const isConfigured = data.configured;
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' : ''}
group relative px-4 py-3 rounded-xl border-2 min-w-[200px] bg-white
transition-all duration-300 ease-out
${selected
? 'border-blue-500 shadow-xl shadow-blue-100 scale-[1.02]'
: 'border-gray-200 shadow-md hover:shadow-lg hover:border-gray-300'}
${!isConfigured ? 'border-dashed opacity-80' : ''}
${hasError ? 'border-red-400 shadow-red-100' : ''}
`}
style={{
animation: 'nodeAppear 0.3s ease-out',
}}
>
{/* Glow effect on selection */}
{selected && (
<div
className="absolute inset-0 -z-10 rounded-xl opacity-20"
style={{
background: 'radial-gradient(ellipse at center, #3b82f6 0%, transparent 70%)',
transform: 'scale(1.3)',
}}
/>
)}
{/* Input handles */}
{inputs > 0 && (
<Handle
type="target"
position={Position.Left}
className="w-3 h-3 !bg-gray-400"
className={`
!w-4 !h-4 !border-2 !border-white !shadow-md
transition-all duration-200
${selected ? '!bg-blue-500' : '!bg-gray-400 group-hover:!bg-gray-500'}
`}
style={{ left: -8 }}
/>
)}
{/* 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 className="flex items-center gap-3 mb-2">
<div className={`
w-8 h-8 rounded-lg ${colorBg} flex items-center justify-center
transition-transform duration-200
${selected ? 'scale-110' : 'group-hover:scale-105'}
`}>
<span className={`text-lg ${color}`}>{icon}</span>
</div>
<div className="flex-1">
<span className="font-semibold text-gray-800 text-sm">{data.label}</span>
{!isConfigured && (
<span className="ml-2 text-xs text-orange-500 animate-pulse">Needs config</span>
)}
</div>
</div>
{/* Content */}
{children && <div className="text-sm text-gray-600">{children}</div>}
{children && (
<div className="text-sm text-gray-600 mt-2 pl-11">
{children}
</div>
)}
{/* Status indicator */}
<div className="absolute top-2 right-2">
{hasError ? (
<span className="w-2 h-2 bg-red-500 rounded-full block animate-pulse" />
) : isConfigured ? (
<span className="w-2 h-2 bg-green-500 rounded-full block" />
) : (
<span className="w-2 h-2 bg-orange-400 rounded-full block" />
)}
</div>
{/* Errors */}
{data.errors?.length ? (
<div className="mt-2 text-xs text-red-500">
{data.errors[0]}
{hasError && (
<div className="mt-3 pt-2 border-t border-red-100">
<p className="text-xs text-red-600 flex items-center gap-1">
<span></span>
{data.errors?.[0]}
</p>
</div>
) : null}
)}
{/* Output handles */}
{outputs > 0 && (
<Handle
type="source"
position={Position.Right}
className="w-3 h-3 !bg-gray-400"
className={`
!w-4 !h-4 !border-2 !border-white !shadow-md
transition-all duration-200
${selected ? '!bg-blue-500' : '!bg-gray-400 group-hover:!bg-gray-500'}
`}
style={{ right: -8 }}
/>
)}
{/* Inline styles for animation */}
<style>{`
@keyframes nodeAppear {
from {
opacity: 0;
transform: scale(0.9) translateY(10px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
`}</style>
</div>
);
}

View File

@@ -6,7 +6,7 @@ import { ConstraintNodeData } from '../../../lib/canvas/schema';
function ConstraintNodeComponent(props: NodeProps<ConstraintNodeData>) {
const { data } = props;
return (
<BaseNode {...props} icon={<span>🚧</span>} color="text-orange-600">
<BaseNode {...props} icon={<span>🚧</span>} color="text-orange-600" colorBg="bg-orange-50">
{data.name && <div>{data.name}</div>}
{data.operator && data.value !== undefined && (
<div className="text-xs text-gray-400">

View File

@@ -6,7 +6,7 @@ import { DesignVarNodeData } from '../../../lib/canvas/schema';
function DesignVarNodeComponent(props: NodeProps<DesignVarNodeData>) {
const { data } = props;
return (
<BaseNode {...props} icon={<span>📐</span>} color="text-green-600">
<BaseNode {...props} icon={<span>📐</span>} color="text-green-600" colorBg="bg-green-50">
{data.expressionName && <div className="font-mono">{data.expressionName}</div>}
{data.minValue !== undefined && data.maxValue !== undefined && (
<div className="text-xs text-gray-400">

View File

@@ -6,7 +6,7 @@ import { ExtractorNodeData } from '../../../lib/canvas/schema';
function ExtractorNodeComponent(props: NodeProps<ExtractorNodeData>) {
const { data } = props;
return (
<BaseNode {...props} icon={<span>🔬</span>} color="text-cyan-600">
<BaseNode {...props} icon={<span>🔬</span>} color="text-cyan-600" colorBg="bg-cyan-50">
{data.extractorName && <div>{data.extractorName}</div>}
{data.extractorId && (
<div className="text-xs text-gray-400">{data.extractorId}</div>

View File

@@ -6,7 +6,7 @@ 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}>
<BaseNode {...props} icon={<span>📦</span>} color="text-blue-600" colorBg="bg-blue-50" inputs={0}>
{data.filePath && (
<div className="truncate max-w-[150px]">{data.filePath.split('/').pop()}</div>
)}

View File

@@ -6,7 +6,7 @@ import { ObjectiveNodeData } from '../../../lib/canvas/schema';
function ObjectiveNodeComponent(props: NodeProps<ObjectiveNodeData>) {
const { data } = props;
return (
<BaseNode {...props} icon={<span>🎯</span>} color="text-red-600">
<BaseNode {...props} icon={<span>🎯</span>} color="text-red-600" colorBg="bg-red-50">
{data.name && <div>{data.name}</div>}
{data.direction && (
<div className="text-xs text-gray-400">

View File

@@ -6,7 +6,7 @@ import { SolverNodeData } from '../../../lib/canvas/schema';
function SolverNodeComponent(props: NodeProps<SolverNodeData>) {
const { data } = props;
return (
<BaseNode {...props} icon={<span></span>} color="text-purple-600">
<BaseNode {...props} icon={<span></span>} color="text-purple-600" colorBg="bg-purple-50">
{data.solverType && <div>{data.solverType}</div>}
</BaseNode>
);

View File

@@ -6,7 +6,7 @@ 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}>
<BaseNode {...props} icon={<span>🚀</span>} color="text-pink-600" colorBg="bg-pink-50" outputs={0}>
<div>{data.enabled ? 'Enabled' : 'Disabled'}</div>
{data.enabled && data.modelType && (
<div className="text-xs text-gray-400">{data.modelType}</div>