From 5bd142780f9de545dd2dfedae5dabc0e45fb438c Mon Sep 17 00:00:00 2001 From: Anto01 Date: Wed, 14 Jan 2026 20:30:28 -0500 Subject: [PATCH] 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 --- .../frontend/src/components/canvas/index.ts | 9 + .../components/canvas/nodes/AlgorithmNode.tsx | 2 +- .../src/components/canvas/nodes/BaseNode.tsx | 107 +++++-- .../canvas/nodes/ConstraintNode.tsx | 2 +- .../components/canvas/nodes/DesignVarNode.tsx | 2 +- .../components/canvas/nodes/ExtractorNode.tsx | 2 +- .../src/components/canvas/nodes/ModelNode.tsx | 2 +- .../components/canvas/nodes/ObjectiveNode.tsx | 2 +- .../components/canvas/nodes/SolverNode.tsx | 2 +- .../components/canvas/nodes/SurrogateNode.tsx | 2 +- .../canvas/panels/ConfigImporter.tsx | 242 +++++++++++++++ .../canvas/panels/TemplateSelector.tsx | 224 ++++++++++++++ .../frontend/src/hooks/index.ts | 5 + .../frontend/src/hooks/useCanvasStore.ts | 260 +++++++++++++++- .../frontend/src/hooks/useIntentParser.ts | 172 +++++++++++ .../frontend/src/lib/canvas/index.ts | 5 + .../frontend/src/lib/canvas/templates.ts | 278 ++++++++++++++++++ .../frontend/src/pages/CanvasView.tsx | 106 ++++++- 18 files changed, 1389 insertions(+), 35 deletions(-) create mode 100644 atomizer-dashboard/frontend/src/components/canvas/panels/ConfigImporter.tsx create mode 100644 atomizer-dashboard/frontend/src/components/canvas/panels/TemplateSelector.tsx create mode 100644 atomizer-dashboard/frontend/src/hooks/index.ts create mode 100644 atomizer-dashboard/frontend/src/hooks/useIntentParser.ts create mode 100644 atomizer-dashboard/frontend/src/lib/canvas/index.ts create mode 100644 atomizer-dashboard/frontend/src/lib/canvas/templates.ts diff --git a/atomizer-dashboard/frontend/src/components/canvas/index.ts b/atomizer-dashboard/frontend/src/components/canvas/index.ts index 753a0264..2d8ab409 100644 --- a/atomizer-dashboard/frontend/src/components/canvas/index.ts +++ b/atomizer-dashboard/frontend/src/components/canvas/index.ts @@ -1,7 +1,16 @@ +// Main Canvas Component export { AtomizerCanvas } from './AtomizerCanvas'; + +// Palette export { NodePalette } from './palette/NodePalette'; + +// Panels export { NodeConfigPanel } from './panels/NodeConfigPanel'; export { ValidationPanel } from './panels/ValidationPanel'; export { ExecuteDialog } from './panels/ExecuteDialog'; export { ChatPanel } from './panels/ChatPanel'; +export { ConfigImporter } from './panels/ConfigImporter'; +export { TemplateSelector } from './panels/TemplateSelector'; + +// Nodes export * from './nodes'; diff --git a/atomizer-dashboard/frontend/src/components/canvas/nodes/AlgorithmNode.tsx b/atomizer-dashboard/frontend/src/components/canvas/nodes/AlgorithmNode.tsx index 474b513a..b2aaee33 100644 --- a/atomizer-dashboard/frontend/src/components/canvas/nodes/AlgorithmNode.tsx +++ b/atomizer-dashboard/frontend/src/components/canvas/nodes/AlgorithmNode.tsx @@ -6,7 +6,7 @@ import { AlgorithmNodeData } from '../../../lib/canvas/schema'; function AlgorithmNodeComponent(props: NodeProps) { const { data } = props; return ( - 🧠} color="text-indigo-600"> + 🧠} color="text-indigo-600" colorBg="bg-indigo-50"> {data.method &&
{data.method}
} {data.maxTrials && (
{data.maxTrials} trials
diff --git a/atomizer-dashboard/frontend/src/components/canvas/nodes/BaseNode.tsx b/atomizer-dashboard/frontend/src/components/canvas/nodes/BaseNode.tsx index 532f77c5..94348b78 100644 --- a/atomizer-dashboard/frontend/src/components/canvas/nodes/BaseNode.tsx +++ b/atomizer-dashboard/frontend/src/components/canvas/nodes/BaseNode.tsx @@ -5,6 +5,7 @@ import { BaseNodeData } from '../../../lib/canvas/schema'; interface BaseNodeProps extends NodeProps { 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 (
+ {/* Glow effect on selection */} + {selected && ( +
+ )} + {/* Input handles */} {inputs > 0 && ( )} {/* Header */} -
- {icon} - {data.label} - {!data.configured && ( - ! - )} +
+
+ {icon} +
+
+ {data.label} + {!isConfigured && ( + Needs config + )} +
{/* Content */} - {children &&
{children}
} + {children && ( +
+ {children} +
+ )} + + {/* Status indicator */} +
+ {hasError ? ( + + ) : isConfigured ? ( + + ) : ( + + )} +
{/* Errors */} - {data.errors?.length ? ( -
- {data.errors[0]} + {hasError && ( +
+

+ + {data.errors?.[0]} +

- ) : null} + )} {/* Output handles */} {outputs > 0 && ( )} + + {/* Inline styles for animation */} +
); } diff --git a/atomizer-dashboard/frontend/src/components/canvas/nodes/ConstraintNode.tsx b/atomizer-dashboard/frontend/src/components/canvas/nodes/ConstraintNode.tsx index 5efe6cfd..2a5c0c7b 100644 --- a/atomizer-dashboard/frontend/src/components/canvas/nodes/ConstraintNode.tsx +++ b/atomizer-dashboard/frontend/src/components/canvas/nodes/ConstraintNode.tsx @@ -6,7 +6,7 @@ import { ConstraintNodeData } from '../../../lib/canvas/schema'; function ConstraintNodeComponent(props: NodeProps) { const { data } = props; return ( - 🚧} color="text-orange-600"> + 🚧} color="text-orange-600" colorBg="bg-orange-50"> {data.name &&
{data.name}
} {data.operator && data.value !== undefined && (
diff --git a/atomizer-dashboard/frontend/src/components/canvas/nodes/DesignVarNode.tsx b/atomizer-dashboard/frontend/src/components/canvas/nodes/DesignVarNode.tsx index 07c5aaef..4007b90c 100644 --- a/atomizer-dashboard/frontend/src/components/canvas/nodes/DesignVarNode.tsx +++ b/atomizer-dashboard/frontend/src/components/canvas/nodes/DesignVarNode.tsx @@ -6,7 +6,7 @@ import { DesignVarNodeData } from '../../../lib/canvas/schema'; function DesignVarNodeComponent(props: NodeProps) { const { data } = props; return ( - 📐} color="text-green-600"> + 📐} color="text-green-600" colorBg="bg-green-50"> {data.expressionName &&
{data.expressionName}
} {data.minValue !== undefined && data.maxValue !== undefined && (
diff --git a/atomizer-dashboard/frontend/src/components/canvas/nodes/ExtractorNode.tsx b/atomizer-dashboard/frontend/src/components/canvas/nodes/ExtractorNode.tsx index b1c5a95b..108eb327 100644 --- a/atomizer-dashboard/frontend/src/components/canvas/nodes/ExtractorNode.tsx +++ b/atomizer-dashboard/frontend/src/components/canvas/nodes/ExtractorNode.tsx @@ -6,7 +6,7 @@ import { ExtractorNodeData } from '../../../lib/canvas/schema'; function ExtractorNodeComponent(props: NodeProps) { const { data } = props; return ( - 🔬} color="text-cyan-600"> + 🔬} color="text-cyan-600" colorBg="bg-cyan-50"> {data.extractorName &&
{data.extractorName}
} {data.extractorId && (
{data.extractorId}
diff --git a/atomizer-dashboard/frontend/src/components/canvas/nodes/ModelNode.tsx b/atomizer-dashboard/frontend/src/components/canvas/nodes/ModelNode.tsx index b1749c96..24707b39 100644 --- a/atomizer-dashboard/frontend/src/components/canvas/nodes/ModelNode.tsx +++ b/atomizer-dashboard/frontend/src/components/canvas/nodes/ModelNode.tsx @@ -6,7 +6,7 @@ import { ModelNodeData } from '../../../lib/canvas/schema'; function ModelNodeComponent(props: NodeProps) { const { data } = props; return ( - 📦} color="text-blue-600" inputs={0}> + 📦} color="text-blue-600" colorBg="bg-blue-50" inputs={0}> {data.filePath && (
{data.filePath.split('/').pop()}
)} diff --git a/atomizer-dashboard/frontend/src/components/canvas/nodes/ObjectiveNode.tsx b/atomizer-dashboard/frontend/src/components/canvas/nodes/ObjectiveNode.tsx index 5b5b76e8..77acd42f 100644 --- a/atomizer-dashboard/frontend/src/components/canvas/nodes/ObjectiveNode.tsx +++ b/atomizer-dashboard/frontend/src/components/canvas/nodes/ObjectiveNode.tsx @@ -6,7 +6,7 @@ import { ObjectiveNodeData } from '../../../lib/canvas/schema'; function ObjectiveNodeComponent(props: NodeProps) { const { data } = props; return ( - 🎯} color="text-red-600"> + 🎯} color="text-red-600" colorBg="bg-red-50"> {data.name &&
{data.name}
} {data.direction && (
diff --git a/atomizer-dashboard/frontend/src/components/canvas/nodes/SolverNode.tsx b/atomizer-dashboard/frontend/src/components/canvas/nodes/SolverNode.tsx index 18852804..dfa08559 100644 --- a/atomizer-dashboard/frontend/src/components/canvas/nodes/SolverNode.tsx +++ b/atomizer-dashboard/frontend/src/components/canvas/nodes/SolverNode.tsx @@ -6,7 +6,7 @@ import { SolverNodeData } from '../../../lib/canvas/schema'; function SolverNodeComponent(props: NodeProps) { const { data } = props; return ( - ⚙️} color="text-purple-600"> + ⚙️} color="text-purple-600" colorBg="bg-purple-50"> {data.solverType &&
{data.solverType}
}
); diff --git a/atomizer-dashboard/frontend/src/components/canvas/nodes/SurrogateNode.tsx b/atomizer-dashboard/frontend/src/components/canvas/nodes/SurrogateNode.tsx index 2aa5218e..a6272ec8 100644 --- a/atomizer-dashboard/frontend/src/components/canvas/nodes/SurrogateNode.tsx +++ b/atomizer-dashboard/frontend/src/components/canvas/nodes/SurrogateNode.tsx @@ -6,7 +6,7 @@ import { SurrogateNodeData } from '../../../lib/canvas/schema'; function SurrogateNodeComponent(props: NodeProps) { const { data } = props; return ( - 🚀} color="text-pink-600" outputs={0}> + 🚀} color="text-pink-600" colorBg="bg-pink-50" outputs={0}>
{data.enabled ? 'Enabled' : 'Disabled'}
{data.enabled && data.modelType && (
{data.modelType}
diff --git a/atomizer-dashboard/frontend/src/components/canvas/panels/ConfigImporter.tsx b/atomizer-dashboard/frontend/src/components/canvas/panels/ConfigImporter.tsx new file mode 100644 index 00000000..eb8d44af --- /dev/null +++ b/atomizer-dashboard/frontend/src/components/canvas/panels/ConfigImporter.tsx @@ -0,0 +1,242 @@ +/** + * ConfigImporter - Import optimization configs into canvas + * + * Supports: + * - File upload (optimization_config.json) + * - Paste JSON directly + * - Load from study directory + */ + +import { useState, useRef } from 'react'; +import { useCanvasStore, OptimizationConfig } from '../../../hooks/useCanvasStore'; + +interface ConfigImporterProps { + isOpen: boolean; + onClose: () => void; + onImport: (source: string) => void; +} + +export function ConfigImporter({ isOpen, onClose, onImport }: ConfigImporterProps) { + const [tab, setTab] = useState<'file' | 'paste' | 'study'>('file'); + const [jsonText, setJsonText] = useState(''); + const [studyPath, setStudyPath] = useState(''); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const fileInputRef = useRef(null); + + const { loadFromConfig } = useCanvasStore(); + + if (!isOpen) return null; + + const handleFileSelect = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + setError(null); + setIsLoading(true); + + try { + const text = await file.text(); + const config = JSON.parse(text) as OptimizationConfig; + validateConfig(config); + loadFromConfig(config); + onImport(`File: ${file.name}`); + handleClose(); + } catch (err) { + setError(err instanceof Error ? err.message : 'Invalid JSON file'); + } finally { + setIsLoading(false); + } + }; + + const handlePasteImport = () => { + setError(null); + + try { + const config = JSON.parse(jsonText) as OptimizationConfig; + validateConfig(config); + loadFromConfig(config); + onImport('Pasted JSON'); + handleClose(); + } catch (err) { + setError(err instanceof Error ? err.message : 'Invalid JSON'); + } + }; + + const handleStudyLoad = async () => { + setError(null); + setIsLoading(true); + + try { + // Call backend API to load study config + const response = await fetch(`/api/studies/${encodeURIComponent(studyPath)}/config`); + + if (!response.ok) { + throw new Error(`Study not found: ${studyPath}`); + } + + const config = await response.json() as OptimizationConfig; + validateConfig(config); + loadFromConfig(config); + onImport(`Study: ${studyPath}`); + handleClose(); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load study'); + } finally { + setIsLoading(false); + } + }; + + const validateConfig = (config: OptimizationConfig) => { + if (!config) { + throw new Error('Empty configuration'); + } + + // Must have at least one design variable or objective + const hasDesignVars = config.design_variables && config.design_variables.length > 0; + const hasObjectives = config.objectives && config.objectives.length > 0; + + if (!hasDesignVars && !hasObjectives) { + throw new Error('Configuration must have design variables or objectives'); + } + }; + + const handleClose = () => { + setJsonText(''); + setStudyPath(''); + setError(null); + setTab('file'); + onClose(); + }; + + return ( +
+
+ {/* Header */} +
+

Import Configuration

+ +
+ + {/* Tabs */} +
+ {[ + { id: 'file', label: 'Upload File' }, + { id: 'paste', label: 'Paste JSON' }, + { id: 'study', label: 'Load Study' }, + ].map((t) => ( + + ))} +
+ + {/* Content */} +
+ {/* File Upload Tab */} + {tab === 'file' && ( +
+

+ Upload an optimization_config.json file + from an existing study. +

+ + +
+ )} + + {/* Paste JSON Tab */} + {tab === 'paste' && ( +
+

+ Paste your optimization configuration JSON below. +

+