Files
Atomizer/docs/plans/RALPH_LOOP_CANVAS_FIX.md

1215 lines
39 KiB
Markdown

# Ralph Loop: Canvas Integration & Styling Fix
## Status: COMPLETED
**Completed:** 2026-01-14
**Commit:** `9f3ac280` - feat: Add Canvas dark theme styling and Setup page integration
### Summary of Changes:
- NodeConfigPanel: Dark theme styling (`bg-dark-850`, white text, dark inputs)
- BaseNode: Dark background, white text, primary selection glow
- AtomizerCanvas: Dark ReactFlow background, styled Controls/MiniMap, dark buttons
- NodePalette: Dark sidebar with hover states on draggable items
- ValidationPanel: Semi-transparent error/warning panels with backdrop blur
- ChatPanel: Dark message area with themed welcome state
- ExecuteDialog: Dark modal with primary button styling
- ConfigImporter: Dark tabs, inputs, file upload zone
- TemplateSelector: Dark template cards with category pills
- Setup.tsx: Added Configuration/Canvas Builder tab switcher
All acceptance criteria passed. Build successful, pushed to both remotes.
---
**Copy everything below the line into Claude Code CLI:**
```powershell
cd C:\Users\antoi\Atomizer
claude --dangerously-skip-permissions
```
---
You are executing an autonomous development session to **fix and integrate the Canvas** into the Atomizer Dashboard.
## Mission
1. **Fix Canvas styling** to match dashboard dark theme (Atomaster)
2. **Integrate Canvas into Setup page** as a tab
3. **Make nodes clickable and configurable** (fix visibility issues)
4. **Connect Canvas to existing Claude CLI chat** (same pattern as ChatPane)
5. **Complete Canvas functionality** (all node types configurable)
## Session Configuration
```
Working Directory: C:/Users/antoi/Atomizer
Frontend: atomizer-dashboard/frontend/
Backend: atomizer-dashboard/backend/
Python: C:/Users/antoi/anaconda3/envs/atomizer/python.exe
Git Remotes: origin (Gitea), github (GitHub) - push to BOTH
```
## Rules
1. **TodoWrite** - Track ALL tasks, mark complete immediately
2. **Test builds** - Run `npm run build` after major changes
3. **Commit when done** - Push to both remotes
4. **Match existing patterns** - Use same styling as existing components
5. **No new dependencies** - Use what's already installed
---
# CURRENT ISSUES (Root Causes)
## Issue 1: Canvas Panel Invisible
**File:** `frontend/src/components/canvas/panels/NodeConfigPanel.tsx`
**Problem:** Uses light theme (`bg-white`, `text-gray-800`) on dark background
```tsx
// WRONG (current)
<div className="w-80 bg-white border-l border-gray-200 p-4 overflow-y-auto">
<h3 className="font-semibold text-gray-800">
// RIGHT (should be)
<div className="w-80 bg-dark-850 border-l border-dark-700 p-4 overflow-y-auto">
<h3 className="font-semibold text-white">
```
## Issue 2: Input Fields Unstyled
**Problem:** No dark theme styling on inputs
```tsx
// WRONG
<input className="w-full px-3 py-2 border rounded-lg" />
// RIGHT
<input className="w-full px-3 py-2 bg-dark-800 border border-dark-600 rounded-lg text-white placeholder-dark-400 focus:border-primary-500 focus:ring-1 focus:ring-primary-500" />
```
## Issue 3: Canvas Buttons Wrong Colors
**File:** `frontend/src/components/canvas/AtomizerCanvas.tsx`
**Problem:** Uses `bg-blue-100`, `bg-gray-100` instead of theme colors
## Issue 4: Canvas Not Integrated
**Problem:** Standalone `/canvas` page, not part of Setup flow
---
# ATOMASTER THEME REFERENCE
## Color Palette
```css
/* Backgrounds */
bg-dark-950: #050a12 /* Deepest */
bg-dark-900: #080f1a /* Main background */
bg-dark-850: #0a1420 /* Cards/panels */
bg-dark-800: #0d1a2d /* Elevated */
bg-dark-700: #152238 /* Borders/dividers */
/* Primary (Cyan) */
bg-primary-500: #00d4e6 /* Main accent */
bg-primary-600: #0891b2 /* Darker */
bg-primary-400: #22d3ee /* Lighter */
/* Text */
text-white /* Headings */
text-dark-200: #94a3b8 /* Body text */
text-dark-300: #64748b /* Secondary */
text-dark-400: #475569 /* Placeholder */
/* Status */
text-green-400: #4ade80 /* Success */
text-yellow-400: #facc15 /* Warning */
text-red-400: #f87171 /* Error */
```
## Component Patterns
### Input Fields
```tsx
<input
className="w-full px-3 py-2 bg-dark-800 border border-dark-600 rounded-lg
text-white placeholder-dark-400
focus:border-primary-500 focus:ring-1 focus:ring-primary-500"
/>
```
### Select Fields
```tsx
<select
className="w-full px-3 py-2 bg-dark-800 border border-dark-600 rounded-lg
text-white focus:border-primary-500"
>
```
### Labels
```tsx
<label className="block text-sm font-medium text-dark-300 mb-1">
```
### Primary Button
```tsx
<button className="px-4 py-2 bg-primary-500 text-white rounded-lg
hover:bg-primary-600 transition-colors">
```
### Secondary Button
```tsx
<button className="px-4 py-2 bg-dark-700 text-white rounded-lg
hover:bg-dark-600 transition-colors">
```
### Cards/Panels
```tsx
<div className="bg-dark-850 border border-dark-700 rounded-lg p-4">
```
### Glass Effect (for overlays)
```tsx
<div className="bg-dark-850/80 backdrop-blur-sm border border-dark-700 rounded-lg">
```
---
# TASK LIST
## Phase 1: Fix NodeConfigPanel Styling
### T1.1 - Update NodeConfigPanel.tsx
**File:** `frontend/src/components/canvas/panels/NodeConfigPanel.tsx`
Replace the entire file with properly styled version:
```tsx
import { useCanvasStore } from '../../../hooks/useCanvasStore';
import { CanvasNodeData } from '../../../lib/canvas/schema';
interface NodeConfigPanelProps {
nodeId: string;
}
export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) {
const { nodes, updateNodeData, deleteSelected } = useCanvasStore();
const node = nodes.find((n) => n.id === nodeId);
if (!node) return null;
const { data } = node;
const handleChange = (field: string, value: unknown) => {
updateNodeData(nodeId, { [field]: value, configured: true });
};
// Input class for consistency
const inputClass = "w-full px-3 py-2 bg-dark-800 border border-dark-600 rounded-lg text-white placeholder-dark-400 focus:border-primary-500 focus:ring-1 focus:ring-primary-500 transition-colors";
const selectClass = "w-full px-3 py-2 bg-dark-800 border border-dark-600 rounded-lg text-white focus:border-primary-500 transition-colors";
const labelClass = "block text-sm font-medium text-dark-300 mb-1";
return (
<div className="w-80 bg-dark-850 border-l border-dark-700 p-4 overflow-y-auto">
{/* Header */}
<div className="flex justify-between items-center mb-6 pb-4 border-b border-dark-700">
<div>
<h3 className="font-semibold text-white">Configure Node</h3>
<p className="text-sm text-dark-400">{data.type}</p>
</div>
<button
onClick={deleteSelected}
className="px-3 py-1 text-sm text-red-400 hover:text-red-300 hover:bg-red-500/10 rounded transition-colors"
>
Delete
</button>
</div>
<div className="space-y-4">
{/* Common: Label */}
<div>
<label className={labelClass}>Label</label>
<input
type="text"
value={data.label}
onChange={(e) => handleChange('label', e.target.value)}
className={inputClass}
/>
</div>
{/* Model Node */}
{data.type === 'model' && (
<>
<div>
<label className={labelClass}>File Path</label>
<input
type="text"
value={(data as any).filePath || ''}
onChange={(e) => handleChange('filePath', e.target.value)}
placeholder="path/to/model.prt"
className={`${inputClass} font-mono text-sm`}
/>
</div>
<div>
<label className={labelClass}>File Type</label>
<select
value={(data as any).fileType || ''}
onChange={(e) => handleChange('fileType', e.target.value)}
className={selectClass}
>
<option value="">Select type...</option>
<option value="prt">Part (.prt)</option>
<option value="fem">FEM (.fem)</option>
<option value="sim">Simulation (.sim)</option>
</select>
</div>
</>
)}
{/* Solver Node */}
{data.type === 'solver' && (
<div>
<label className={labelClass}>Solution Type</label>
<select
value={(data as any).solverType || ''}
onChange={(e) => handleChange('solverType', e.target.value)}
className={selectClass}
>
<option value="">Select solution...</option>
<option value="SOL101">SOL 101 - Linear Static</option>
<option value="SOL103">SOL 103 - Modal Analysis</option>
<option value="SOL105">SOL 105 - Buckling</option>
<option value="SOL106">SOL 106 - Nonlinear Static</option>
<option value="SOL111">SOL 111 - Frequency Response</option>
<option value="SOL112">SOL 112 - Transient Response</option>
</select>
</div>
)}
{/* Design Variable Node */}
{data.type === 'designVar' && (
<>
<div>
<label className={labelClass}>Expression Name</label>
<input
type="text"
value={(data as any).expressionName || ''}
onChange={(e) => handleChange('expressionName', e.target.value)}
placeholder="thickness"
className={`${inputClass} font-mono`}
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className={labelClass}>Min Value</label>
<input
type="number"
value={(data as any).minValue ?? ''}
onChange={(e) => handleChange('minValue', parseFloat(e.target.value) || 0)}
placeholder="0"
className={inputClass}
/>
</div>
<div>
<label className={labelClass}>Max Value</label>
<input
type="number"
value={(data as any).maxValue ?? ''}
onChange={(e) => handleChange('maxValue', parseFloat(e.target.value) || 100)}
placeholder="100"
className={inputClass}
/>
</div>
</div>
<div>
<label className={labelClass}>Unit (optional)</label>
<input
type="text"
value={(data as any).unit || ''}
onChange={(e) => handleChange('unit', e.target.value)}
placeholder="mm"
className={inputClass}
/>
</div>
</>
)}
{/* Extractor Node */}
{data.type === 'extractor' && (
<>
<div>
<label className={labelClass}>Extractor Type</label>
<select
value={(data as any).extractorId || ''}
onChange={(e) => {
const id = e.target.value;
const names: Record<string, string> = {
'E1': 'Displacement',
'E2': 'Frequency',
'E3': 'Stress (Solid)',
'E4': 'Mass (BDF)',
'E5': 'Mass (CAD)',
'E8': 'Zernike RMS',
'E9': 'Zernike P-V',
'E10': 'Zernike Coefficients',
};
handleChange('extractorId', id);
handleChange('extractorName', names[id] || id);
}}
className={selectClass}
>
<option value="">Select extractor...</option>
<optgroup label="Displacement">
<option value="E1">E1 - Displacement</option>
</optgroup>
<optgroup label="Frequency">
<option value="E2">E2 - Natural Frequency</option>
</optgroup>
<optgroup label="Stress">
<option value="E3">E3 - Solid Stress</option>
</optgroup>
<optgroup label="Mass">
<option value="E4">E4 - Mass from BDF</option>
<option value="E5">E5 - Mass from CAD</option>
</optgroup>
<optgroup label="Zernike (Optics)">
<option value="E8">E8 - Zernike RMS WFE</option>
<option value="E9">E9 - Zernike P-V</option>
<option value="E10">E10 - Zernike Coefficients</option>
</optgroup>
</select>
</div>
{(data as any).extractorId && (
<div className="p-3 bg-dark-800 rounded-lg">
<p className="text-sm text-dark-300">
Selected: <span className="text-primary-400 font-medium">{(data as any).extractorName}</span>
</p>
</div>
)}
</>
)}
{/* Objective Node */}
{data.type === 'objective' && (
<>
<div>
<label className={labelClass}>Objective Name</label>
<input
type="text"
value={(data as any).name || ''}
onChange={(e) => handleChange('name', e.target.value)}
placeholder="mass"
className={inputClass}
/>
</div>
<div>
<label className={labelClass}>Direction</label>
<div className="grid grid-cols-2 gap-2">
<button
type="button"
onClick={() => handleChange('direction', 'minimize')}
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
(data as any).direction === 'minimize'
? 'bg-primary-500 text-white'
: 'bg-dark-700 text-dark-300 hover:bg-dark-600'
}`}
>
Minimize
</button>
<button
type="button"
onClick={() => handleChange('direction', 'maximize')}
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
(data as any).direction === 'maximize'
? 'bg-primary-500 text-white'
: 'bg-dark-700 text-dark-300 hover:bg-dark-600'
}`}
>
Maximize
</button>
</div>
</div>
<div>
<label className={labelClass}>Weight (for multi-objective)</label>
<input
type="number"
value={(data as any).weight ?? 1}
onChange={(e) => handleChange('weight', parseFloat(e.target.value) || 1)}
min="0"
step="0.1"
className={inputClass}
/>
</div>
</>
)}
{/* Constraint Node */}
{data.type === 'constraint' && (
<>
<div>
<label className={labelClass}>Constraint Name</label>
<input
type="text"
value={(data as any).name || ''}
onChange={(e) => handleChange('name', e.target.value)}
placeholder="max_stress"
className={inputClass}
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className={labelClass}>Operator</label>
<select
value={(data as any).operator || '<='}
onChange={(e) => handleChange('operator', e.target.value)}
className={selectClass}
>
<option value="<">&lt; (less than)</option>
<option value="<=">&le; (at most)</option>
<option value=">">&gt; (greater than)</option>
<option value=">=">&ge; (at least)</option>
<option value="==">= (equals)</option>
</select>
</div>
<div>
<label className={labelClass}>Value</label>
<input
type="number"
value={(data as any).value ?? ''}
onChange={(e) => handleChange('value', parseFloat(e.target.value) || 0)}
placeholder="250"
className={inputClass}
/>
</div>
</div>
</>
)}
{/* Algorithm Node */}
{data.type === 'algorithm' && (
<>
<div>
<label className={labelClass}>Optimization Method</label>
<select
value={(data as any).method || ''}
onChange={(e) => handleChange('method', e.target.value)}
className={selectClass}
>
<option value="">Select method...</option>
<option value="TPE">TPE (Tree Parzen Estimator)</option>
<option value="CMA-ES">CMA-ES (Evolution Strategy)</option>
<option value="NSGA-II">NSGA-II (Multi-Objective)</option>
<option value="GP-BO">GP-BO (Gaussian Process)</option>
<option value="RandomSearch">Random Search</option>
<option value="IMSO">IMSO (Intelligent Adaptive)</option>
</select>
</div>
<div>
<label className={labelClass}>Maximum Trials</label>
<input
type="number"
value={(data as any).maxTrials ?? ''}
onChange={(e) => handleChange('maxTrials', parseInt(e.target.value) || 100)}
placeholder="100"
min="1"
className={inputClass}
/>
</div>
{(data as any).method && (
<div className="p-3 bg-dark-800 rounded-lg">
<p className="text-xs text-dark-400">
{(data as any).method === 'TPE' && 'Best for single-objective with continuous variables'}
{(data as any).method === 'CMA-ES' && 'Best for continuous optimization, handles correlation'}
{(data as any).method === 'NSGA-II' && 'Required for multi-objective (Pareto front)'}
{(data as any).method === 'GP-BO' && 'Best for expensive evaluations, few trials'}
{(data as any).method === 'RandomSearch' && 'Baseline comparison, no intelligence'}
{(data as any).method === 'IMSO' && 'Auto-selects best method based on problem'}
</p>
</div>
)}
</>
)}
{/* Surrogate Node */}
{data.type === 'surrogate' && (
<>
<div>
<label className={labelClass}>Enable Neural Surrogate</label>
<div className="grid grid-cols-2 gap-2">
<button
type="button"
onClick={() => handleChange('enabled', true)}
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
(data as any).enabled === true
? 'bg-green-500 text-white'
: 'bg-dark-700 text-dark-300 hover:bg-dark-600'
}`}
>
Enabled
</button>
<button
type="button"
onClick={() => handleChange('enabled', false)}
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
(data as any).enabled === false
? 'bg-dark-600 text-white'
: 'bg-dark-700 text-dark-300 hover:bg-dark-600'
}`}
>
Disabled
</button>
</div>
</div>
{(data as any).enabled && (
<>
<div>
<label className={labelClass}>Surrogate Type</label>
<select
value={(data as any).modelType || ''}
onChange={(e) => handleChange('modelType', e.target.value)}
className={selectClass}
>
<option value="">Auto-select</option>
<option value="MLP">MLP (Fast, general)</option>
<option value="GNN">GNN (Zernike/mesh-aware)</option>
<option value="Ensemble">Ensemble (Most accurate)</option>
</select>
</div>
<div>
<label className={labelClass}>Min Trials Before Activation</label>
<input
type="number"
value={(data as any).minTrials ?? 20}
onChange={(e) => handleChange('minTrials', parseInt(e.target.value) || 20)}
min="10"
className={inputClass}
/>
</div>
</>
)}
</>
)}
{/* Configuration Status */}
<div className="pt-4 mt-4 border-t border-dark-700">
<div className="flex items-center gap-2">
{data.configured ? (
<>
<div className="w-2 h-2 bg-green-400 rounded-full"></div>
<span className="text-sm text-green-400">Configured</span>
</>
) : (
<>
<div className="w-2 h-2 bg-yellow-400 rounded-full"></div>
<span className="text-sm text-yellow-400">Needs configuration</span>
</>
)}
</div>
</div>
</div>
</div>
);
}
```
---
## Phase 2: Fix Canvas Node Styling
### T2.1 - Update BaseNode.tsx
**File:** `frontend/src/components/canvas/nodes/BaseNode.tsx`
```tsx
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-dark-850 shadow-lg transition-all duration-200
${selected ? 'border-primary-500 shadow-primary-500/20' : 'border-dark-600'}
${!data.configured ? 'border-dashed' : ''}
${data.errors?.length ? 'border-red-500' : ''}
`}
>
{/* Input handles */}
{inputs > 0 && (
<Handle
type="target"
position={Position.Left}
className="w-3 h-3 !bg-dark-400 !border-dark-600 hover:!bg-primary-500"
/>
)}
{/* Header */}
<div className="flex items-center gap-2 mb-2">
<span className={`text-lg ${color}`}>{icon}</span>
<span className="font-medium text-white">{data.label}</span>
{!data.configured && (
<span className="text-xs text-yellow-400"></span>
)}
</div>
{/* Content */}
{children && <div className="text-sm text-dark-300">{children}</div>}
{/* Errors */}
{data.errors?.length ? (
<div className="mt-2 text-xs text-red-400">
{data.errors[0]}
</div>
) : null}
{/* Output handles */}
{outputs > 0 && (
<Handle
type="source"
position={Position.Right}
className="w-3 h-3 !bg-dark-400 !border-dark-600 hover:!bg-primary-500"
/>
)}
</div>
);
}
export const BaseNode = memo(BaseNodeComponent);
```
---
## Phase 3: Fix Main Canvas Component
### T3.1 - Update AtomizerCanvas.tsx
**File:** `frontend/src/components/canvas/AtomizerCanvas.tsx`
Fix button styling and overall theme:
```tsx
import { useCallback, useRef, DragEvent, useState } from 'react';
import ReactFlow, {
Background,
Controls,
MiniMap,
ReactFlowProvider,
ReactFlowInstance,
} from 'reactflow';
import 'reactflow/dist/style.css';
import { nodeTypes } from './nodes';
import { NodePalette } from './palette/NodePalette';
import { NodeConfigPanel } from './panels/NodeConfigPanel';
import { ValidationPanel } from './panels/ValidationPanel';
import { useCanvasStore } from '../../hooks/useCanvasStore';
import { NodeType } from '../../lib/canvas/schema';
import { Play, CheckCircle, Sparkles, X } from 'lucide-react';
function CanvasFlow() {
const reactFlowWrapper = useRef<HTMLDivElement>(null);
const reactFlowInstance = useRef<ReactFlowInstance | null>(null);
const [showValidation, setShowValidation] = useState(false);
const {
nodes,
edges,
selectedNode,
onNodesChange,
onEdgesChange,
onConnect,
addNode,
selectNode,
validation,
validate,
toIntent,
} = useCanvasStore();
const onDragOver = useCallback((event: DragEvent) => {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
}, []);
const onDrop = useCallback(
(event: DragEvent) => {
event.preventDefault();
const type = event.dataTransfer.getData('application/reactflow') as NodeType;
if (!type || !reactFlowInstance.current || !reactFlowWrapper.current) return;
const bounds = reactFlowWrapper.current.getBoundingClientRect();
const position = reactFlowInstance.current.screenToFlowPosition({
x: event.clientX - bounds.left,
y: event.clientY - bounds.top,
});
addNode(type, position);
},
[addNode]
);
const onNodeClick = useCallback(
(_: React.MouseEvent, node: { id: string }) => {
selectNode(node.id);
},
[selectNode]
);
const onPaneClick = useCallback(() => {
selectNode(null);
}, [selectNode]);
const handleValidate = () => {
const result = validate();
setShowValidation(true);
};
const handleExecute = () => {
const result = validate();
if (result.valid) {
const intent = toIntent();
console.log('Executing intent:', intent);
// TODO: Connect to chat
}
};
return (
<div className="flex h-full bg-dark-900">
{/* Left: Node Palette */}
<NodePalette />
{/* Center: Canvas */}
<div className="flex-1 relative" ref={reactFlowWrapper}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onInit={(instance) => { reactFlowInstance.current = instance; }}
onDragOver={onDragOver}
onDrop={onDrop}
onNodeClick={onNodeClick}
onPaneClick={onPaneClick}
nodeTypes={nodeTypes}
fitView
className="bg-dark-900"
>
<Background color="#1e293b" gap={20} size={1} />
<Controls className="!bg-dark-800 !border-dark-700 !rounded-lg [&>button]:!bg-dark-700 [&>button]:!border-dark-600 [&>button]:!text-white [&>button:hover]:!bg-dark-600" />
<MiniMap
className="!bg-dark-800 !border-dark-700"
nodeColor="#0891b2"
maskColor="rgba(0,0,0,0.8)"
/>
</ReactFlow>
{/* Action Buttons */}
<div className="absolute bottom-4 right-4 flex gap-2">
<button
onClick={handleValidate}
className="flex items-center gap-2 px-4 py-2 bg-dark-700 text-white rounded-lg hover:bg-dark-600 transition-colors"
>
<CheckCircle size={16} />
Validate
</button>
<button
onClick={() => {
const result = validate();
if (result.valid) {
const intent = toIntent();
console.log('Analyzing:', intent);
}
}}
disabled={nodes.length === 0}
className="flex items-center gap-2 px-4 py-2 bg-dark-700 text-white rounded-lg hover:bg-dark-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<Sparkles size={16} />
Analyze
</button>
<button
onClick={handleExecute}
disabled={!validation.valid}
className={`flex items-center gap-2 px-4 py-2 rounded-lg transition-colors ${
validation.valid
? 'bg-primary-500 text-white hover:bg-primary-600'
: 'bg-dark-700 text-dark-400 cursor-not-allowed'
}`}
>
<Play size={16} />
Execute
</button>
</div>
{/* Validation Messages */}
{showValidation && (validation.errors.length > 0 || validation.warnings.length > 0) && (
<div className="absolute top-4 left-1/2 transform -translate-x-1/2">
<ValidationPanel
validation={validation}
onClose={() => setShowValidation(false)}
/>
</div>
)}
</div>
{/* Right: Config Panel */}
{selectedNode && <NodeConfigPanel nodeId={selectedNode} />}
</div>
);
}
export function AtomizerCanvas() {
return (
<ReactFlowProvider>
<CanvasFlow />
</ReactFlowProvider>
);
}
```
---
## Phase 4: Fix Node Palette Styling
### T4.1 - Update NodePalette.tsx
**File:** `frontend/src/components/canvas/palette/NodePalette.tsx`
```tsx
import { DragEvent } from 'react';
import { NodeType } from '../../../lib/canvas/schema';
import {
Box, Settings, Variable, Microscope, Target,
AlertTriangle, Brain, Zap
} from 'lucide-react';
interface PaletteItem {
type: NodeType;
label: string;
icon: React.ReactNode;
description: string;
color: string;
}
const PALETTE_ITEMS: PaletteItem[] = [
{ type: 'model', label: 'Model', icon: <Box size={20} />, description: 'NX model file', color: 'text-blue-400' },
{ type: 'solver', label: 'Solver', icon: <Settings size={20} />, description: 'Nastran solution', color: 'text-purple-400' },
{ type: 'designVar', label: 'Design Variable', icon: <Variable size={20} />, description: 'Parameter to vary', color: 'text-green-400' },
{ type: 'extractor', label: 'Extractor', icon: <Microscope size={20} />, description: 'Physics extraction', color: 'text-cyan-400' },
{ type: 'objective', label: 'Objective', icon: <Target size={20} />, description: 'Optimization goal', color: 'text-red-400' },
{ type: 'constraint', label: 'Constraint', icon: <AlertTriangle size={20} />, description: 'Limit condition', color: 'text-orange-400' },
{ type: 'algorithm', label: 'Algorithm', icon: <Brain size={20} />, description: 'Optimization method', color: 'text-indigo-400' },
{ type: 'surrogate', label: 'Surrogate', icon: <Zap size={20} />, description: 'Neural acceleration', color: 'text-pink-400' },
];
export function NodePalette() {
const onDragStart = (event: DragEvent, nodeType: NodeType) => {
event.dataTransfer.setData('application/reactflow', nodeType);
event.dataTransfer.effectAllowed = 'move';
};
return (
<div className="w-64 bg-dark-850 border-r border-dark-700 p-4 overflow-y-auto">
<h3 className="text-xs font-semibold text-dark-400 uppercase tracking-wider mb-4">
Components
</h3>
<p className="text-xs text-dark-500 mb-4">
Drag nodes to the canvas to build your optimization workflow
</p>
<div className="space-y-2">
{PALETTE_ITEMS.map((item) => (
<div
key={item.type}
draggable
onDragStart={(e) => onDragStart(e, item.type)}
className="flex items-center gap-3 p-3 bg-dark-800 rounded-lg border border-dark-700
cursor-grab hover:border-primary-500/50 hover:bg-dark-750
active:cursor-grabbing transition-all group"
>
<span className={`${item.color} group-hover:scale-110 transition-transform`}>
{item.icon}
</span>
<div className="flex-1 min-w-0">
<div className="font-medium text-white text-sm">{item.label}</div>
<div className="text-xs text-dark-400 truncate">{item.description}</div>
</div>
</div>
))}
</div>
</div>
);
}
```
---
## Phase 5: Fix Validation Panel
### T5.1 - Update ValidationPanel.tsx
**File:** `frontend/src/components/canvas/panels/ValidationPanel.tsx`
```tsx
import { X, AlertCircle, AlertTriangle } from 'lucide-react';
import { ValidationResult } from '../../../lib/canvas/validation';
interface ValidationPanelProps {
validation: ValidationResult;
onClose?: () => void;
}
export function ValidationPanel({ validation, onClose }: ValidationPanelProps) {
if (validation.errors.length === 0 && validation.warnings.length === 0) {
return (
<div className="bg-green-500/10 border border-green-500/30 rounded-lg p-4 backdrop-blur-sm">
<div className="flex items-center gap-2 text-green-400">
<AlertCircle size={16} />
<span className="font-medium">Validation passed</span>
</div>
</div>
);
}
return (
<div className="max-w-md w-full bg-dark-850/95 backdrop-blur-sm border border-dark-700 rounded-lg shadow-xl">
{/* Header */}
<div className="flex items-center justify-between p-3 border-b border-dark-700">
<span className="font-medium text-white">Validation Results</span>
{onClose && (
<button
onClick={onClose}
className="p-1 text-dark-400 hover:text-white transition-colors"
>
<X size={16} />
</button>
)}
</div>
<div className="p-4 space-y-3">
{/* Errors */}
{validation.errors.length > 0 && (
<div className="space-y-2">
<div className="flex items-center gap-2 text-red-400 text-sm font-medium">
<AlertCircle size={14} />
Errors ({validation.errors.length})
</div>
<ul className="space-y-1">
{validation.errors.map((error, i) => (
<li key={i} className="text-sm text-red-300 pl-5 before:content-['•'] before:absolute before:left-2 relative">
{error}
</li>
))}
</ul>
</div>
)}
{/* Warnings */}
{validation.warnings.length > 0 && (
<div className="space-y-2">
<div className="flex items-center gap-2 text-yellow-400 text-sm font-medium">
<AlertTriangle size={14} />
Warnings ({validation.warnings.length})
</div>
<ul className="space-y-1">
{validation.warnings.map((warning, i) => (
<li key={i} className="text-sm text-yellow-300 pl-5 before:content-['•'] before:absolute before:left-2 relative">
{warning}
</li>
))}
</ul>
</div>
)}
</div>
</div>
);
}
```
---
## Phase 6: Integrate Canvas into Setup Page
### T6.1 - Update Setup.tsx to Add Canvas Tab
**File:** `frontend/src/pages/Setup.tsx`
Add a tab system to switch between Form view and Canvas view:
Find the header section and add tabs. The Setup page should have:
1. A tab bar with "Configuration" and "Visual Canvas" tabs
2. When "Visual Canvas" is selected, show the AtomizerCanvas component
3. Keep the existing form view as default
Add this import at top:
```tsx
import { AtomizerCanvas } from '../components/canvas/AtomizerCanvas';
```
Add tab state:
```tsx
const [activeTab, setActiveTab] = useState<'config' | 'canvas'>('config');
```
Add tab bar after the header:
```tsx
{/* Tab Bar */}
<div className="flex gap-1 p-1 bg-dark-800 rounded-lg mb-6">
<button
onClick={() => setActiveTab('config')}
className={`flex-1 px-4 py-2 rounded-md text-sm font-medium transition-colors ${
activeTab === 'config'
? 'bg-primary-500 text-white'
: 'text-dark-300 hover:text-white hover:bg-dark-700'
}`}
>
Configuration
</button>
<button
onClick={() => setActiveTab('canvas')}
className={`flex-1 px-4 py-2 rounded-md text-sm font-medium transition-colors ${
activeTab === 'canvas'
? 'bg-primary-500 text-white'
: 'text-dark-300 hover:text-white hover:bg-dark-700'
}`}
>
Visual Canvas
</button>
</div>
```
Wrap existing content in conditional:
```tsx
{activeTab === 'config' ? (
{/* Existing form content */}
) : (
<div className="h-[calc(100vh-200px)] rounded-lg overflow-hidden border border-dark-700">
<AtomizerCanvas />
</div>
)}
```
---
## Phase 7: Add Canvas Chat Integration
### T7.1 - Create useCanvasChat Hook
**File:** `frontend/src/hooks/useCanvasChat.ts`
This hook bridges Canvas with the existing chat system:
```tsx
import { useCallback } from 'react';
import { useChat } from './useChat';
import { OptimizationIntent, formatIntentForChat } from '../lib/canvas/intent';
export function useCanvasChat() {
const { sendMessage, isConnected, isThinking } = useChat();
const validateIntent = useCallback(async (intent: OptimizationIntent) => {
const message = `Please validate this optimization intent and tell me if there are any issues:\n\n${JSON.stringify(intent, null, 2)}`;
await sendMessage(message);
}, [sendMessage]);
const executeIntent = useCallback(async (intent: OptimizationIntent, autoRun: boolean = false) => {
const action = autoRun ? 'create and run' : 'create';
const message = `Please ${action} this optimization study from the following canvas intent:\n\n${JSON.stringify(intent, null, 2)}`;
await sendMessage(message);
}, [sendMessage]);
const analyzeIntent = useCallback(async (intent: OptimizationIntent) => {
const message = `Analyze this optimization setup and provide recommendations:\n\n${JSON.stringify(intent, null, 2)}\n\nConsider:\n- Is the algorithm appropriate for this problem?\n- Are the design variable ranges reasonable?\n- Any missing constraints or objectives?`;
await sendMessage(message);
}, [sendMessage]);
return {
validateIntent,
executeIntent,
analyzeIntent,
isConnected,
isThinking,
};
}
```
---
## Acceptance Criteria
After all changes:
```bash
# 1. Build frontend
cd atomizer-dashboard/frontend
npm run build
# Expected: No errors
# 2. Start dev server
npm run dev
# Expected: Vite starts
# 3. Browser tests:
# - Go to /setup (or study setup page)
# - Click "Visual Canvas" tab
# - Canvas appears with dark theme
# - Drag nodes from palette
# - Click on nodes - config panel appears (visible, dark themed)
# - Configure node properties
# - Click Validate - see validation messages
# - All text is readable (white on dark)
```
---
## Commit
When all tests pass:
```bash
git add .
git commit -m "feat: Fix Canvas styling and integrate into Setup page
- Fix NodeConfigPanel dark theme (bg-dark-850, white text)
- Fix BaseNode styling to match dashboard
- Fix NodePalette with proper colors
- Fix ValidationPanel dark theme
- Fix AtomizerCanvas buttons and controls
- Add Canvas tab to Setup page
- Add useCanvasChat hook for Claude integration
All components now match Atomaster dark theme.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>"
git push origin main && git push github main
```
---
# BEGIN EXECUTION
1. Use TodoWrite to track each phase
2. Read existing files before editing
3. Apply changes phase by phase
4. Test build after each major component
5. Verify dark theme is consistent
6. Test node selection and configuration
7. Commit when all acceptance criteria pass
**GO.**