1215 lines
39 KiB
Markdown
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="<">< (less than)</option>
|
||
|
|
<option value="<=">≤ (at most)</option>
|
||
|
|
<option value=">">> (greater than)</option>
|
||
|
|
<option value=">=">≥ (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.**
|