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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user