From 62284a995eb9b0be464de3ee3e91f790d31f02bc Mon Sep 17 00:00:00 2001 From: Anto01 Date: Fri, 16 Jan 2026 11:34:41 -0500 Subject: [PATCH] feat(canvas): Canvas V3 - Bug fixes and study workflow improvements Bug Fixes: - Fix Atomizer Assistant error with reconnect button and error state handling - Enable connection/edge deletion with keyboard Delete/Backspace keys - Fix drag & drop positioning using screenToFlowPosition correctly - Fix loadFromConfig to create all node types and edges properly UI/UX Improvements: - Minimal responsive header with context breadcrumb - Better contrast with white text on dark backgrounds - Larger font sizes in NodePalette for readability - Study-aware header showing selected study name New Features: - Enhanced ExecuteDialog with Create/Update mode toggle - Select existing study to update or create new study - Home page Canvas Builder button for quick access - Home navigation button in CanvasView header Co-Authored-By: Claude Opus 4.5 --- .../src/components/canvas/AtomizerCanvas.tsx | 98 +++++-- .../src/components/canvas/nodes/BaseNode.tsx | 22 +- .../components/canvas/palette/NodePalette.tsx | 16 +- .../canvas/panels/ExecuteDialog.tsx | 215 +++++++++++---- .../frontend/src/hooks/useCanvasStore.ts | 245 +++++++++++++----- .../frontend/src/pages/CanvasView.tsx | 59 +++-- .../frontend/src/pages/Home.tsx | 43 ++- 7 files changed, 525 insertions(+), 173 deletions(-) diff --git a/atomizer-dashboard/frontend/src/components/canvas/AtomizerCanvas.tsx b/atomizer-dashboard/frontend/src/components/canvas/AtomizerCanvas.tsx index 5a02fea9..843ecd2a 100644 --- a/atomizer-dashboard/frontend/src/components/canvas/AtomizerCanvas.tsx +++ b/atomizer-dashboard/frontend/src/components/canvas/AtomizerCanvas.tsx @@ -1,13 +1,14 @@ -import { useCallback, useRef, useState, DragEvent } from 'react'; +import { useCallback, useRef, useState, useEffect, DragEvent } from 'react'; import ReactFlow, { Background, Controls, MiniMap, ReactFlowProvider, ReactFlowInstance, + Edge, } from 'reactflow'; import 'reactflow/dist/style.css'; -import { MessageCircle, Plug, X } from 'lucide-react'; +import { MessageCircle, Plug, X, AlertCircle, RefreshCw } from 'lucide-react'; import { nodeTypes } from './nodes'; import { NodePalette } from './palette/NodePalette'; @@ -29,16 +30,21 @@ function CanvasFlow() { nodes, edges, selectedNode, + selectedEdge, onNodesChange, onEdgesChange, onConnect, addNode, selectNode, + selectEdge, + deleteSelected, validation, validate, toIntent, } = useCanvasStore(); + const [chatError, setChatError] = useState(null); + const { messages, isThinking, @@ -49,9 +55,19 @@ function CanvasFlow() { analyzeIntent, sendMessage, } = useCanvasChat({ - onError: (error) => console.error('Canvas chat error:', error), + onError: (error) => { + console.error('Canvas chat error:', error); + setChatError(error); + }, }); + const handleReconnect = useCallback(() => { + setChatError(null); + // Force refresh chat connection by toggling panel + setShowChat(false); + setTimeout(() => setShowChat(true), 100); + }, []); + const onDragOver = useCallback((event: DragEvent) => { event.preventDefault(); event.dataTransfer.dropEffect = 'move'; @@ -62,12 +78,12 @@ function CanvasFlow() { event.preventDefault(); const type = event.dataTransfer.getData('application/reactflow') as NodeType; - if (!type || !reactFlowInstance.current || !reactFlowWrapper.current) return; + if (!type || !reactFlowInstance.current) return; - const bounds = reactFlowWrapper.current.getBoundingClientRect(); + // screenToFlowPosition expects screen coordinates directly const position = reactFlowInstance.current.screenToFlowPosition({ - x: event.clientX - bounds.left, - y: event.clientY - bounds.top, + x: event.clientX, + y: event.clientY, }); addNode(type, position); @@ -84,7 +100,32 @@ function CanvasFlow() { const onPaneClick = useCallback(() => { selectNode(null); - }, [selectNode]); + selectEdge(null); + }, [selectNode, selectEdge]); + + const onEdgeClick = useCallback( + (_: React.MouseEvent, edge: Edge) => { + selectEdge(edge.id); + }, + [selectEdge] + ); + + // Keyboard handler for Delete/Backspace + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Delete' || event.key === 'Backspace') { + // Don't delete if focus is on an input + const target = event.target as HTMLElement; + if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) { + return; + } + deleteSelected(); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [deleteSelected]); const handleValidate = () => { const result = validate(); @@ -112,8 +153,9 @@ function CanvasFlow() { } }; - const handleExecute = async (studyName: string, autoRun: boolean) => { + const handleExecute = async (studyName: string, autoRun: boolean, _mode: 'create' | 'update', _existingStudyId?: string) => { const intent = toIntent(); + // For now, both modes use the same executeIntent - backend will handle the mode distinction await executeIntent(intent, studyName, autoRun); setShowExecuteDialog(false); setShowChat(true); @@ -128,7 +170,14 @@ function CanvasFlow() {
({ + ...e, + style: { + stroke: e.id === selectedEdge ? '#60a5fa' : '#6b7280', + strokeWidth: e.id === selectedEdge ? 3 : 2, + }, + animated: e.id === selectedEdge, + }))} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} onConnect={onConnect} @@ -136,9 +185,11 @@ function CanvasFlow() { onDragOver={onDragOver} onDrop={onDrop} onNodeClick={onNodeClick} + onEdgeClick={onEdgeClick} onPaneClick={onPaneClick} nodeTypes={nodeTypes} fitView + deleteKeyCode={null} className="bg-dark-900" > @@ -211,12 +262,27 @@ function CanvasFlow() {
- + {chatError ? ( +
+ +

Connection Error

+

{chatError}

+ +
+ ) : ( + + )} ) : selectedNode ? ( diff --git a/atomizer-dashboard/frontend/src/components/canvas/nodes/BaseNode.tsx b/atomizer-dashboard/frontend/src/components/canvas/nodes/BaseNode.tsx index 171ca3bf..41294d50 100644 --- a/atomizer-dashboard/frontend/src/components/canvas/nodes/BaseNode.tsx +++ b/atomizer-dashboard/frontend/src/components/canvas/nodes/BaseNode.tsx @@ -25,9 +25,9 @@ function BaseNodeComponent({ return (
)} -
+
{icon}
-
{data.label}
+
{data.label}
{!data.configured && ( -
+
)}
{children && ( -
+
{children}
)} {hasErrors && ( -
- +
+ {data.errors![0]}
)} @@ -69,7 +69,7 @@ function BaseNodeComponent({ )}
diff --git a/atomizer-dashboard/frontend/src/components/canvas/palette/NodePalette.tsx b/atomizer-dashboard/frontend/src/components/canvas/palette/NodePalette.tsx index fb203e11..cb79fa5f 100644 --- a/atomizer-dashboard/frontend/src/components/canvas/palette/NodePalette.tsx +++ b/atomizer-dashboard/frontend/src/components/canvas/palette/NodePalette.tsx @@ -37,31 +37,31 @@ export function NodePalette() { }; return ( -
+
-

+

Components

-

+

Drag to canvas

-
+
{PALETTE_ITEMS.map((item) => (
onDragStart(e, item.type)} - className="flex items-center gap-2.5 px-3 py-2.5 bg-dark-800/50 rounded-lg border border-dark-700/50 + className="flex items-center gap-3 px-3 py-3 bg-dark-800/50 rounded-lg border border-dark-700/50 cursor-grab hover:border-primary-500/50 hover:bg-dark-800 active:cursor-grabbing transition-all group" > -
+
{item.icon}
-
{item.label}
-
{item.description}
+
{item.label}
+
{item.description}
))} diff --git a/atomizer-dashboard/frontend/src/components/canvas/panels/ExecuteDialog.tsx b/atomizer-dashboard/frontend/src/components/canvas/panels/ExecuteDialog.tsx index 01b857a5..dd65269d 100644 --- a/atomizer-dashboard/frontend/src/components/canvas/panels/ExecuteDialog.tsx +++ b/atomizer-dashboard/frontend/src/components/canvas/panels/ExecuteDialog.tsx @@ -1,14 +1,19 @@ /** - * Execute Dialog - Prompts for study name before executing canvas intent + * Execute Dialog - Choose to update existing study or create new one */ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; +import { FilePlus, RefreshCw, FolderOpen } from 'lucide-react'; +import { useStudy } from '../../../context/StudyContext'; + +type ExecuteMode = 'create' | 'update'; interface ExecuteDialogProps { isOpen: boolean; onClose: () => void; - onExecute: (studyName: string, autoRun: boolean) => void; + onExecute: (studyName: string, autoRun: boolean, mode: ExecuteMode, existingStudyId?: string) => void; isExecuting: boolean; + defaultStudyId?: string; // Pre-selected study when editing } export function ExecuteDialog({ @@ -16,31 +21,68 @@ export function ExecuteDialog({ onClose, onExecute, isExecuting, + defaultStudyId, }: ExecuteDialogProps) { + const { studies } = useStudy(); + const [mode, setMode] = useState('create'); const [studyName, setStudyName] = useState(''); + const [selectedStudyId, setSelectedStudyId] = useState(''); const [autoRun, setAutoRun] = useState(false); const [error, setError] = useState(null); + // Reset state when dialog opens + useEffect(() => { + if (isOpen) { + if (defaultStudyId) { + setMode('update'); + setSelectedStudyId(defaultStudyId); + } else { + setMode('create'); + setSelectedStudyId(studies[0]?.id || ''); + } + setStudyName(''); + setAutoRun(false); + setError(null); + } + }, [isOpen, defaultStudyId, studies]); + if (!isOpen) return null; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); - // Validate study name - const trimmed = studyName.trim(); - if (!trimmed) { - setError('Study name is required'); - return; - } + if (mode === 'create') { + // Validate study name + const trimmed = studyName.trim(); + if (!trimmed) { + setError('Study name is required'); + return; + } - // Check for valid snake_case - if (!/^[a-z][a-z0-9_]*$/.test(trimmed)) { - setError('Study name must be snake_case (lowercase letters, numbers, underscores)'); - return; - } + // Check for valid snake_case + if (!/^[a-z][a-z0-9_]*$/.test(trimmed)) { + setError('Study name must be snake_case (lowercase letters, numbers, underscores)'); + return; + } - setError(null); - onExecute(trimmed, autoRun); + setError(null); + onExecute(trimmed, autoRun, 'create'); + } else { + // Update mode + if (!selectedStudyId) { + setError('Please select a study to update'); + return; + } + + const selectedStudy = studies.find(s => s.id === selectedStudyId); + if (!selectedStudy) { + setError('Selected study not found'); + return; + } + + setError(null); + onExecute(selectedStudy.id, autoRun, 'update', selectedStudyId); + } }; const handleClose = () => { @@ -52,39 +94,106 @@ export function ExecuteDialog({ return (
-
-

+
+

Execute with Claude

+

+ Choose to create a new study or update an existing one +

+ + {/* Mode Tabs */} +
+ + +
-
- - setStudyName(e.target.value.toLowerCase().replace(/\s+/g, '_'))} - placeholder="my_optimization_study" - className="w-full px-3 py-2 bg-dark-800 border border-dark-600 text-white placeholder-dark-400 rounded-lg font-mono focus:ring-2 focus:ring-primary-500 focus:border-primary-500 focus:outline-none transition-colors" - disabled={isExecuting} - autoFocus - /> - {error && ( -

{error}

- )} -

- Use snake_case (e.g., bracket_mass_v1, mirror_wfe_optimization) -

-
+ {mode === 'create' ? ( + /* Create New Study */ +
+ + setStudyName(e.target.value.toLowerCase().replace(/\s+/g, '_'))} + placeholder="my_optimization_study" + className="w-full px-3 py-2.5 bg-dark-800 border border-dark-600 text-white placeholder-dark-500 rounded-lg font-mono focus:ring-2 focus:ring-primary-500 focus:border-primary-500 focus:outline-none transition-colors" + disabled={isExecuting} + autoFocus + /> +

+ Use snake_case (e.g., bracket_mass_v1, mirror_wfe_optimization) +

+
+ ) : ( + /* Update Existing Study */ +
+ +
+ + +
+

+ Warning: This will overwrite the study's optimization_config.json +

+
+ )} + + {error && ( +
+

{error}

+
+ )}
-
@@ -103,23 +212,33 @@ export function ExecuteDialog({ type="button" onClick={handleClose} disabled={isExecuting} - className="px-4 py-2 text-dark-300 hover:text-white disabled:opacity-50 transition-colors" + className="px-4 py-2.5 text-dark-300 hover:text-white disabled:opacity-50 transition-colors" > Cancel diff --git a/atomizer-dashboard/frontend/src/hooks/useCanvasStore.ts b/atomizer-dashboard/frontend/src/hooks/useCanvasStore.ts index fb3b0e64..0c564e55 100644 --- a/atomizer-dashboard/frontend/src/hooks/useCanvasStore.ts +++ b/atomizer-dashboard/frontend/src/hooks/useCanvasStore.ts @@ -8,6 +8,7 @@ interface CanvasState { nodes: Node[]; edges: Edge[]; selectedNode: string | null; + selectedEdge: string | null; validation: ValidationResult; // Actions @@ -17,7 +18,9 @@ interface CanvasState { addNode: (type: NodeType, position: { x: number; y: number }) => void; updateNodeData: (nodeId: string, data: Partial) => void; selectNode: (nodeId: string | null) => void; + selectEdge: (edgeId: string | null) => void; deleteSelected: () => void; + deleteEdge: (edgeId: string) => void; validate: () => ValidationResult; toIntent: () => OptimizationIntent; clear: () => void; @@ -94,6 +97,7 @@ export const useCanvasStore = create((set, get) => ({ nodes: [], edges: [], selectedNode: null, + selectedEdge: null, validation: { valid: false, errors: [], warnings: [] }, onNodesChange: (changes) => { @@ -129,11 +133,26 @@ export const useCanvasStore = create((set, get) => ({ }, selectNode: (nodeId) => { - set({ selectedNode: nodeId }); + set({ selectedNode: nodeId, selectedEdge: null }); + }, + + selectEdge: (edgeId) => { + set({ selectedEdge: edgeId, selectedNode: null }); }, deleteSelected: () => { - const { selectedNode, nodes, edges } = get(); + const { selectedNode, selectedEdge, nodes, edges } = get(); + + // Delete selected edge + if (selectedEdge) { + set({ + edges: edges.filter((e) => e.id !== selectedEdge), + selectedEdge: null, + }); + return; + } + + // Delete selected node if (!selectedNode) return; set({ @@ -143,6 +162,13 @@ export const useCanvasStore = create((set, get) => ({ }); }, + deleteEdge: (edgeId) => { + set({ + edges: get().edges.filter((e) => e.id !== edgeId), + selectedEdge: null, + }); + }, + validate: () => { const { nodes, edges } = get(); const result = validateGraph(nodes, edges); @@ -160,6 +186,7 @@ export const useCanvasStore = create((set, get) => ({ nodes: [], edges: [], selectedNode: null, + selectedEdge: null, validation: { valid: false, errors: [], warnings: [] }, }); nodeIdCounter = 0; @@ -305,77 +332,177 @@ export const useCanvasStore = create((set, get) => ({ nodes, edges, selectedNode: null, + selectedEdge: null, validation: { valid: false, errors: [], warnings: [] }, }); }, loadFromConfig: (config) => { - // Convert optimization_config.json format to intent format, then load - const intent: OptimizationIntent = { - version: '1.0', - source: 'canvas', - timestamp: new Date().toISOString(), - model: { - path: config.model?.path, - type: config.model?.type, - }, - solver: { - type: config.solver?.solution ? `SOL${config.solver.solution}` : undefined, - }, - design_variables: (config.design_variables || []).map(dv => ({ - name: dv.expression_name || dv.name, - min: dv.lower, - max: dv.upper, - })), - extractors: [], // Will be inferred from objectives - objectives: (config.objectives || []).map(obj => ({ - name: obj.name, - direction: (obj.direction as 'minimize' | 'maximize') || 'minimize', - weight: obj.weight || 1, - extractor: obj.extractor || '', - })), - constraints: (config.constraints || []).map(con => ({ - name: con.name, - operator: con.type === 'upper' ? '<=' : '>=', - value: con.value || 0, - extractor: con.extractor || '', - })), - optimization: { - method: config.method, - max_trials: config.max_trials, - }, - surrogate: config.surrogate ? { - enabled: true, - type: config.surrogate.type, - min_trials: config.surrogate.min_trials, - } : undefined, + // Complete rewrite: Create all nodes and edges directly from config + nodeIdCounter = 0; + const nodes: Node[] = []; + const edges: Edge[] = []; + + // Column positions for proper layout + const COLS = { + modelDvar: 50, + solver: 280, + extractor: 510, + objCon: 740, + algo: 970, + surrogate: 1200, + }; + const ROW_HEIGHT = 100; + const START_Y = 50; + + // Helper to create node + const createNode = (type: NodeType, x: number, y: number, data: Partial): string => { + const id = getNodeId(); + nodes.push({ + id, + type, + position: { x, y }, + data: { ...getDefaultData(type), ...data, configured: true } as CanvasNodeData, + }); + return id; + }; + + // 1. Model node + const modelId = createNode('model', COLS.modelDvar, START_Y, { + label: config.study_name || 'Model', + filePath: config.model?.path, + fileType: config.model?.type as 'prt' | 'fem' | 'sim' | undefined, + }); + + // 2. Solver node + const solverType = config.solver?.solution ? `SOL${config.solver.solution}` : undefined; + const solverId = createNode('solver', COLS.solver, START_Y, { + label: 'Solver', + solverType: solverType as any, + }); + edges.push({ id: `e_model_solver`, source: modelId, target: solverId }); + + // 3. Design variables (column 0, below model) + let dvRow = 1; + for (const dv of config.design_variables || []) { + const dvId = createNode('designVar', COLS.modelDvar, START_Y + dvRow * ROW_HEIGHT, { + label: dv.expression_name || dv.name, + expressionName: dv.expression_name || dv.name, + minValue: dv.lower, + maxValue: dv.upper, + }); + edges.push({ id: `e_dv_${dvRow}_model`, source: dvId, target: modelId }); + dvRow++; + } + + // 4. Extractors - infer from objectives and constraints + const extractorNames: Record = { + 'E1': 'Displacement', 'E2': 'Frequency', 'E3': 'Solid Stress', + 'E4': 'BDF Mass', 'E5': 'CAD Mass', 'E8': 'Zernike (OP2)', + 'E9': 'Zernike (CSV)', 'E10': 'Zernike (RMS)', }; - // Infer extractors from objectives and constraints const extractorIds = new Set(); - for (const obj of intent.objectives) { + for (const obj of config.objectives || []) { if (obj.extractor) extractorIds.add(obj.extractor); } - for (const con of intent.constraints) { + for (const con of config.constraints || []) { if (con.extractor) extractorIds.add(con.extractor); } - const extractorNames: Record = { - 'E1': 'Displacement', - 'E2': 'Frequency', - 'E3': 'Solid Stress', - 'E4': 'BDF Mass', - 'E5': 'CAD Mass', - 'E8': 'Zernike (OP2)', - 'E9': 'Zernike (CSV)', - 'E10': 'Zernike (RMS)', - }; + // If no extractors found, add a default based on objectives + if (extractorIds.size === 0 && (config.objectives?.length || 0) > 0) { + extractorIds.add('E5'); // Default to CAD Mass + } - intent.extractors = Array.from(extractorIds).map(id => ({ - id, - name: extractorNames[id] || id, - })); + let extRow = 0; + const extractorMap: Record = {}; + for (const extId of extractorIds) { + const nodeId = createNode('extractor', COLS.extractor, START_Y + extRow * ROW_HEIGHT, { + label: extractorNames[extId] || extId, + extractorId: extId, + extractorName: extractorNames[extId] || extId, + }); + extractorMap[extId] = nodeId; + edges.push({ id: `e_solver_ext_${extRow}`, source: solverId, target: nodeId }); + extRow++; + } - get().loadFromIntent(intent); + // 5. Objectives + let objRow = 0; + const objIds: string[] = []; + for (const obj of config.objectives || []) { + const objId = createNode('objective', COLS.objCon, START_Y + objRow * ROW_HEIGHT, { + label: obj.name, + name: obj.name, + direction: (obj.direction as 'minimize' | 'maximize') || 'minimize', + weight: obj.weight || 1, + }); + objIds.push(objId); + + // Connect to extractor + const extNodeId = obj.extractor ? extractorMap[obj.extractor] : Object.values(extractorMap)[0]; + if (extNodeId) { + edges.push({ id: `e_ext_obj_${objRow}`, source: extNodeId, target: objId }); + } + objRow++; + } + + // 6. Constraints + let conRow = objRow; + const conIds: string[] = []; + for (const con of config.constraints || []) { + const conId = createNode('constraint', COLS.objCon, START_Y + conRow * ROW_HEIGHT, { + label: con.name, + name: con.name, + operator: (con.type === 'upper' ? '<=' : '>=') as any, + value: con.value || 0, + }); + conIds.push(conId); + + // Connect to extractor + const extNodeId = con.extractor ? extractorMap[con.extractor] : Object.values(extractorMap)[0]; + if (extNodeId) { + edges.push({ id: `e_ext_con_${conRow}`, source: extNodeId, target: conId }); + } + conRow++; + } + + // 7. Algorithm node + const method = config.method || (config as any).optimization?.sampler || 'TPE'; + const maxTrials = config.max_trials || (config as any).optimization?.n_trials || 100; + const algoId = createNode('algorithm', COLS.algo, START_Y, { + label: 'Algorithm', + method: method as any, + maxTrials: maxTrials, + }); + + // Connect objectives to algorithm + for (let i = 0; i < objIds.length; i++) { + edges.push({ id: `e_obj_${i}_algo`, source: objIds[i], target: algoId }); + } + // Connect constraints to algorithm + for (let i = 0; i < conIds.length; i++) { + edges.push({ id: `e_con_${i}_algo`, source: conIds[i], target: algoId }); + } + + // 8. Surrogate node (if enabled) + if (config.surrogate) { + const surId = createNode('surrogate', COLS.surrogate, START_Y, { + label: 'Surrogate', + enabled: true, + modelType: config.surrogate.type as any, + minTrials: config.surrogate.min_trials, + }); + edges.push({ id: `e_algo_sur`, source: algoId, target: surId }); + } + + set({ + nodes, + edges, + selectedNode: null, + selectedEdge: null, + validation: { valid: false, errors: [], warnings: [] }, + }); }, })); diff --git a/atomizer-dashboard/frontend/src/pages/CanvasView.tsx b/atomizer-dashboard/frontend/src/pages/CanvasView.tsx index c54fb79c..289f099f 100644 --- a/atomizer-dashboard/frontend/src/pages/CanvasView.tsx +++ b/atomizer-dashboard/frontend/src/pages/CanvasView.tsx @@ -1,17 +1,21 @@ import { useState } from 'react'; -import { ClipboardList, Download, Trash2 } from 'lucide-react'; +import { useNavigate } from 'react-router-dom'; +import { ClipboardList, Download, Trash2, Layers, Home, ChevronRight } from 'lucide-react'; import { AtomizerCanvas } from '../components/canvas/AtomizerCanvas'; import { TemplateSelector } from '../components/canvas/panels/TemplateSelector'; import { ConfigImporter } from '../components/canvas/panels/ConfigImporter'; import { useCanvasStore } from '../hooks/useCanvasStore'; +import { useStudy } from '../context/StudyContext'; import { CanvasTemplate } from '../lib/canvas/templates'; export function CanvasView() { const [showTemplates, setShowTemplates] = useState(false); const [showImporter, setShowImporter] = useState(false); const [notification, setNotification] = useState(null); + const navigate = useNavigate(); - const { nodes, clear } = useCanvasStore(); + const { nodes, edges, clear } = useCanvasStore(); + const { selectedStudy } = useStudy(); const handleTemplateSelect = (template: CanvasTemplate) => { showNotification(`Loaded template: ${template.name}`); @@ -35,38 +39,59 @@ export function CanvasView() { return (
- {/* Header with actions */} -
-
-

- Optimization Canvas -

-

- Drag components from the palette to build your optimization workflow -

+ {/* Minimal Header */} +
+
+ {/* Home button */} + + + {/* Breadcrumb */} +
+ + Canvas Builder + {selectedStudy && ( + <> + + + {selectedStudy.name || selectedStudy.id} + + + )} +
+ + {/* Stats */} + + {nodes.length} node{nodes.length !== 1 ? 's' : ''} • {edges.length} edge{edges.length !== 1 ? 's' : ''} +
{/* Action Buttons */}
diff --git a/atomizer-dashboard/frontend/src/pages/Home.tsx b/atomizer-dashboard/frontend/src/pages/Home.tsx index ee24e0c8..262887af 100644 --- a/atomizer-dashboard/frontend/src/pages/Home.tsx +++ b/atomizer-dashboard/frontend/src/pages/Home.tsx @@ -17,7 +17,8 @@ import { Folder, FolderOpen, Maximize2, - X + X, + Layers } from 'lucide-react'; import { useStudy } from '../context/StudyContext'; import { Study } from '../types'; @@ -172,19 +173,33 @@ const Home: React.FC = () => { className="h-10 md:h-12" />
- +
+ + +