chore: Project cleanup and Canvas UX improvements (Phase 7-9)

## Cleanup (v0.5.0)
- Delete 102+ orphaned MCP session temp files
- Remove build artifacts (htmlcov, dist, __pycache__)
- Archive superseded plan docs (RALPH_LOOP V2/V3, CANVAS V3, etc.)
- Move debug/analysis scripts from tests/ to tools/analysis/
- Archive redundant NX journals to archive/nx_journals/
- Archive monolithic PROTOCOL.md to docs/archive/
- Update .gitignore with missing patterns
- Clean old study files (optimization_log_old.txt, run_optimization_old.py)

## Canvas UX (Phases 7-9)
- Phase 7: Resizable panels with localStorage persistence
  - Left sidebar: 200-400px, Right panel: 280-600px
  - New useResizablePanel hook and ResizeHandle component
- Phase 8: Enable all palette items
  - All 8 node types now draggable
  - Singleton logic for model/solver/algorithm/surrogate
- Phase 9: Solver configuration
  - Add SolverEngine type (nxnastran, mscnastran, python, etc.)
  - Add NastranSolutionType (SOL101-SOL200)
  - Engine/solution dropdowns in config panel
  - Python script path support

## Documentation
- Update CHANGELOG.md with recent versions
- Update docs/00_INDEX.md
- Create examples/README.md
- Add docs/plans/CANVAS_UX_IMPROVEMENTS.md
This commit is contained in:
2026-01-24 15:17:34 -05:00
parent 2cb8dccc3a
commit a3f18dc377
38 changed files with 1172 additions and 2570 deletions

View File

@@ -1,452 +0,0 @@
# Canvas UX Design - Study Management Flow
**Created**: January 16, 2026
**Status**: Design Phase
---
## Problem Statement
The Canvas Builder needs clear answers to these questions:
1. **When processing a canvas**, does it overwrite the current study or create a new one?
2. **How do users start fresh** - create a new study from scratch?
3. **How does the Home page** handle study selection vs. creation?
4. **What's the relationship** between Canvas, Dashboard, and study context?
---
## Proposed Solution: Study-Aware Canvas
### Core Concepts
| Concept | Description |
|---------|-------------|
| **Study Context** | Global state tracking which study is "active" |
| **Canvas Mode** | Either "editing existing" or "creating new" |
| **Process Dialog** | Explicit choice: update vs. create new |
| **Study Browser** | Unified view for selecting/creating studies |
---
## User Flows
### Flow 1: Open Existing Study → View/Edit → Run
```
Home Page
├── Study List shows all studies with status
│ • bracket_v3 [Running] 47/100 trials
│ • mirror_wfe_v2 [Paused] 23/50 trials
│ • beam_freq [Complete] 100/100 trials
└── User clicks "bracket_v3"
┌─────────────────────────────────────┐
│ Study: bracket_v3 │
│ │
│ ┌─────────────────────────────────┐ │
│ │ Dashboard │ Results │ Canvas │ │
│ └─────────────────────────────────┘ │
│ │
│ [Run Optimization] [Edit Canvas] │
└─────────────────────────────────────┘
├── "Dashboard" → Live monitoring
├── "Results" → Reports & analysis
└── "Canvas" → View/edit workflow
Canvas loads from optimization_config.json
Shows all nodes + connections
User can modify and re-process
```
### Flow 2: Create New Study from Scratch
```
Home Page
└── User clicks [+ New Study]
┌─────────────────────────────────────┐
│ Create New Study │
│ │
│ Study Name: [my_new_bracket_____] │
│ │
│ Category: [Bracket ▼] │
│ ├─ Bracket │
│ ├─ Beam │
│ ├─ Mirror │
│ └─ + New Category... │
│ │
│ Start with: │
│ ● Blank Canvas │
│ ○ Template: [Mass Minimization ▼] │
│ ○ Copy from: [bracket_v3 ▼] │
│ │
│ [Cancel] [Create Study] │
└─────────────────────────────────────┘
Backend creates folder structure:
studies/Bracket/my_new_bracket/
├── 1_config/
├── 2_iterations/
└── 3_results/
Opens Canvas in "New Study" mode
Header shows: "Creating: my_new_bracket"
Canvas is blank (or has template)
```
### Flow 3: Canvas → Process → Create/Update Decision
```
User is in Canvas (either mode)
└── Clicks [Process with Claude]
┌─────────────────────────────────────────────┐
│ Generate Optimization │
│ │
│ Claude will generate: │
│ • optimization_config.json │
│ • run_optimization.py │
│ │
│ ─────────────────────────────────────────── │
│ │
│ If editing existing study: │
│ ┌─────────────────────────────────────────┐ │
│ │ ● Update current study │ │
│ │ Will modify: bracket_v3 │ │
│ │ ⚠ Overwrites existing config │ │
│ │ │ │
│ │ ○ Create new study │ │
│ │ Name: [bracket_v4_____________] │ │
│ │ □ Copy model files from bracket_v3 │ │
│ └─────────────────────────────────────────┘ │
│ │
│ If creating new study: │
│ ┌─────────────────────────────────────────┐ │
│ │ Creating: my_new_bracket │ │
│ │ │ │
│ │ Model files needed: │
│ │ □ Upload .prt file │
│ │ □ Upload .sim file │
│ │ - or - │
│ │ □ Copy from: [bracket_v3 ▼] │ │
│ └─────────────────────────────────────────┘ │
│ │
│ [Cancel] [Generate & Create] │
└─────────────────────────────────────────────┘
Claude processes intent:
1. Validates configuration
2. Generates optimization_config.json
3. Creates run_optimization.py
4. If new study: switches context to it
Success dialog:
┌─────────────────────────────────────┐
│ ✓ Study Created Successfully │
│ │
│ bracket_v4 is ready to run │
│ │
│ [View Dashboard] [Run Optimization]│
└─────────────────────────────────────┘
```
---
## UI Components
### 1. Home Page (Redesigned)
```
┌─────────────────────────────────────────────────────────────────┐
│ Atomizer [+ New Study]│
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Search studies... 🔍 │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ Recent Studies ││
│ │ ││
│ │ ┌─────────────────────────────────────────────────────────┐││
│ │ │ 🟢 bracket_v3 Running │││
│ │ │ Bracket • 47/100 trials • Best: 2.34 kg │││
│ │ │ [Dashboard] [Canvas] [Results] │││
│ │ └─────────────────────────────────────────────────────────┘││
│ │ ││
│ │ ┌─────────────────────────────────────────────────────────┐││
│ │ │ 🟡 mirror_wfe_v2 Paused │││
│ │ │ Mirror • 23/50 trials • Best: 5.67 nm │││
│ │ │ [Dashboard] [Canvas] [Results] │││
│ │ └─────────────────────────────────────────────────────────┘││
│ │ ││
│ │ ┌─────────────────────────────────────────────────────────┐││
│ │ │ ✓ beam_freq_tuning Completed │││
│ │ │ Beam • 100/100 trials • Best: 0.12 Hz error │││
│ │ │ [Dashboard] [Canvas] [Results] │││
│ │ └─────────────────────────────────────────────────────────┘││
│ └─────────────────────────────────────────────────────────────┘│
│ │
│ Categories: [All] [Bracket] [Beam] [Mirror] │
│ │
└─────────────────────────────────────────────────────────────────┘
```
### 2. Study Header (When Study Selected)
```
┌─────────────────────────────────────────────────────────────────┐
│ ← Back bracket_v3 🟢 Running 47/100 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┬──────────┬──────────┬──────────┐ │
│ │Dashboard │ Results │ Canvas │ Settings │ │
│ └──────────┴──────────┴──────────┴──────────┘ │
│ │
│ [Run Optimization ▶] [Pause ⏸] [Generate Report] │
│ │
└─────────────────────────────────────────────────────────────────┘
```
### 3. Canvas Header (Context-Aware)
**When editing existing study:**
```
┌─────────────────────────────────────────────────────────────────┐
│ Canvas Builder Editing: bracket_v3 │
├─────────────────────────────────────────────────────────────────┤
│ [Templates] [Import] 5 nodes [Validate] [Process ▶] │
└─────────────────────────────────────────────────────────────────┘
```
**When creating new study:**
```
┌─────────────────────────────────────────────────────────────────┐
│ Canvas Builder Creating: my_new_bracket │
├─────────────────────────────────────────────────────────────────┤
│ [Templates] [Import] 0 nodes [Validate] [Create Study ▶] │
└─────────────────────────────────────────────────────────────────┘
```
### 4. Process/Create Dialog
```
┌─────────────────────────────────────────────────────────────────┐
│ Generate Optimization ✕ │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Claude will analyze your workflow and generate: │
│ • optimization_config.json │
│ • run_optimization.py (using Atomizer protocols) │
│ │
│ ───────────────────────────────────────────────────────────────│
│ │
│ Save to: │
│ │
│ ○ Update existing study │
│ └─ bracket_v3 (overwrites current config) │
│ │
│ ● Create new study │
│ └─ Study name: [bracket_v4_______________] │
│ Category: [Bracket ▼] │
│ ☑ Copy model files from bracket_v3 │
│ │
│ ───────────────────────────────────────────────────────────────│
│ │
│ After creation: │
│ ☑ Open new study automatically │
│ ☐ Start optimization immediately │
│ │
│ [Cancel] [Generate] │
└─────────────────────────────────────────────────────────────────┘
```
---
## Technical Architecture
### 1. Global Study Context
```typescript
// contexts/StudyContext.tsx
interface Study {
path: string; // "Bracket/bracket_v3"
name: string; // "bracket_v3"
category: string; // "Bracket"
status: 'not_started' | 'running' | 'paused' | 'completed';
trialCount: number;
maxTrials: number;
bestValue?: number;
hasConfig: boolean;
}
interface StudyContextState {
// Current selection
currentStudy: Study | null;
isCreatingNew: boolean;
pendingStudyName: string | null;
// Actions
selectStudy: (studyPath: string) => Promise<void>;
createNewStudy: (name: string, category: string) => Promise<Study>;
closeStudy: () => void;
refreshStudies: () => Promise<void>;
// All studies
studies: Study[];
categories: string[];
}
```
### 2. Canvas Store Enhancement
```typescript
// hooks/useCanvasStore.ts additions
interface CanvasState {
// Existing...
nodes: Node[];
edges: Edge[];
// New: Study context
sourceStudyPath: string | null; // Where loaded from (null = new)
isModified: boolean; // Has unsaved changes
// Actions
loadFromStudy: (studyPath: string) => Promise<void>;
saveToStudy: (studyPath: string, overwrite: boolean) => Promise<void>;
markClean: () => void;
}
```
### 3. Backend API Additions
```python
# api/routes/studies.py
@router.post("/")
async def create_study(request: CreateStudyRequest):
"""Create new study folder structure."""
# Creates:
# studies/{category}/{name}/
# ├── 1_config/
# ├── 2_iterations/
# └── 3_results/
pass
@router.post("/{study_path}/copy-models")
async def copy_model_files(study_path: str, source_study: str):
"""Copy .prt, .sim, .fem files from another study."""
pass
@router.post("/{study_path}/generate")
async def generate_from_intent(study_path: str, intent: dict, overwrite: bool = False):
"""Generate optimization_config.json and run_optimization.py from canvas intent."""
pass
@router.get("/categories")
async def list_categories():
"""List all study categories."""
pass
```
### 4. File Structure
When a new study is created:
```
studies/
└── Bracket/
└── my_new_bracket/
├── 1_config/
│ └── (empty until "Process")
├── 2_iterations/
│ └── (empty until optimization runs)
└── 3_results/
└── (empty until optimization runs)
```
After "Process with Claude":
```
studies/
└── Bracket/
└── my_new_bracket/
├── 1_config/
│ ├── optimization_config.json ← Generated
│ └── workflow_config.json ← Generated (optional)
├── 2_iterations/
├── 3_results/
├── model.prt ← Copied from source
├── model_sim1.sim ← Copied from source
└── run_optimization.py ← Generated
```
---
## Workflow Summary
### Starting Points
| Entry Point | Context | Canvas Mode |
|-------------|---------|-------------|
| Home → Click study → Canvas | Existing study | Edit existing |
| Home → New Study → Blank | New study | Create new |
| Home → New Study → Template | New study | Create new (pre-filled) |
| Home → New Study → Copy from | New study | Create new (pre-filled) |
### Process Outcomes
| Canvas Mode | Process Choice | Result |
|-------------|----------------|--------|
| Edit existing | Update current | Overwrites config in same study |
| Edit existing | Create new | Creates new study, switches to it |
| Create new | (only option) | Creates files in new study |
### Post-Process Navigation
| Option | Action |
|--------|--------|
| Open Dashboard | Navigate to /dashboard with new study |
| Run Optimization | Start run_optimization.py, show Dashboard |
| Stay in Canvas | Keep editing (for iterations) |
---
## Benefits
1. **Clear mental model**: Users always know if they're editing or creating
2. **No accidental overwrites**: Explicit choice with warning
3. **Version control friendly**: Easy to create v2, v3, etc.
4. **Discoverable**: Home page shows all studies at a glance
5. **Flexible entry points**: Multiple ways to start/continue work
---
## Implementation Priority
1. **Home Page redesign** with study list and "New Study" button
2. **Study context** global state
3. **Create Study dialog** with options
4. **Process dialog** with update/create choice
5. **Canvas context awareness** (header shows current study)
6. **Backend endpoints** for study creation and file generation
---
*This design enables a clean, intuitive workflow from study discovery to optimization execution.*

View File

@@ -0,0 +1,445 @@
# Canvas UX Improvements - Master Plan
**Created:** January 2026
**Status:** Planning
**Branch:** `feature/studio-enhancement`
## Overview
This plan addresses three major UX issues in the Canvas Builder:
1. **Resizable Panels** - Right pane (chat/config) is fixed at 384px, cannot be adjusted
2. **Disabled Palette Items** - Model, Solver, Algorithm, Surrogate are grayed out and not draggable
3. **Solver Type Selection** - Solver node should allow selection of solver type (NX Nastran, Python, etc.)
---
## Phase 7: Resizable Panels
### Current State
- Left sidebar: Fixed 240px (expanded) or 56px (collapsed)
- Right panel (Chat/Config): Fixed 384px
- Canvas: Takes remaining space
### Requirements
- Users should be able to drag panel edges to resize
- Minimum/maximum constraints for usability
- Persist panel sizes in localStorage
- Smooth resize with proper cursor feedback
### Implementation
#### 7.1 Create Resizable Panel Hook
```typescript
// hooks/useResizablePanel.ts
interface ResizablePanelState {
width: number;
isDragging: boolean;
startDrag: (e: React.MouseEvent) => void;
}
function useResizablePanel(
key: string,
defaultWidth: number,
minWidth: number,
maxWidth: number
): ResizablePanelState
```
#### 7.2 Update CanvasView Layout
- Wrap left sidebar with resizer
- Wrap right panel with resizer
- Add visual drag handles (thin border that highlights on hover)
- Add cursor: col-resize on hover
#### 7.3 Files to Modify
| File | Changes |
|------|---------|
| `hooks/useResizablePanel.ts` | NEW - Resize hook with localStorage persistence |
| `pages/CanvasView.tsx` | Add resizers to left/right panels |
| `components/canvas/ResizeHandle.tsx` | NEW - Visual resize handle component |
#### 7.4 Constraints
| Panel | Min | Default | Max |
|-------|-----|---------|-----|
| Left (Palette/Files) | 200px | 240px | 400px |
| Right (Chat/Config) | 280px | 384px | 600px |
---
## Phase 8: Enable All Palette Items
### Current State
- Model, Solver, Algorithm, Surrogate are marked `canAdd: false`
- They appear grayed out with "Auto-created" text
- Users cannot drag them to canvas
### Problem Analysis
These nodes were marked as "synthetic" because they're derived from:
- **Model**: From `spec.model.sim.path`
- **Solver**: From model's solution type
- **Algorithm**: From `spec.optimization.algorithm`
- **Surrogate**: From `spec.optimization.surrogate`
However, users need to:
1. Add a Model node when creating a new study from scratch
2. Configure the Solver type
3. Choose an Algorithm
4. Enable/configure Surrogate
### Solution: Make All Items Draggable
#### 8.1 Update NodePalette
```typescript
// All items should be draggable
export const PALETTE_ITEMS: PaletteItem[] = [
{
type: 'model',
label: 'Model',
canAdd: true, // Changed from false
description: 'NX/FEM model file',
},
{
type: 'solver',
label: 'Solver',
canAdd: true, // Changed from false
description: 'Analysis solver',
},
// ... etc
];
```
#### 8.2 Handle "Singleton" Nodes
Some nodes should only exist once on the canvas:
- Model (only one model per study)
- Solver (one solver)
- Algorithm (one algorithm config)
- Surrogate (optional, one)
When user drags a singleton that already exists:
- Option A: Show warning toast "Model already exists"
- Option B: Select the existing node instead of creating new
- **Recommended**: Option B (select existing)
#### 8.3 Update SpecRenderer Drop Handler
```typescript
const onDrop = useCallback(async (event: DragEvent) => {
const type = event.dataTransfer.getData('application/reactflow');
// Check if singleton already exists
const SINGLETON_TYPES = ['model', 'solver', 'algorithm', 'surrogate'];
if (SINGLETON_TYPES.includes(type)) {
const existingNode = nodes.find(n => n.type === type);
if (existingNode) {
selectNode(existingNode.id);
showNotification(`${type} already exists - selected it`);
return;
}
}
// Create new node...
}, [...]);
```
#### 8.4 Default Data for New Node Types
```typescript
function getDefaultNodeData(type: NodeType, position) {
switch (type) {
case 'model':
return {
name: 'Model',
sim: { path: '', solver: 'nastran' },
canvas_position: position,
};
case 'solver':
return {
name: 'Solver',
type: 'nxnastran', // Default solver
solution_type: 'SOL101',
canvas_position: position,
};
case 'algorithm':
return {
name: 'Algorithm',
type: 'TPE',
budget: { max_trials: 100 },
canvas_position: position,
};
case 'surrogate':
return {
name: 'Surrogate',
enabled: false,
model_type: 'MLP',
min_trials: 20,
canvas_position: position,
};
// ... existing cases
}
}
```
#### 8.5 Files to Modify
| File | Changes |
|------|---------|
| `components/canvas/palette/NodePalette.tsx` | Set `canAdd: true` for all items |
| `components/canvas/SpecRenderer.tsx` | Handle singleton logic in onDrop |
| `lib/spec/converter.ts` | Ensure synthetic nodes have proper IDs |
| `hooks/useSpecStore.ts` | Add model/solver/algorithm to addNode support |
---
## Phase 9: Solver Type Selection
### Current State
- Solver node shows auto-detected solution type (SOL101, etc.)
- No ability to change solver engine or configure it
### Requirements
1. Allow selection of solver engine type
2. Configure solution type
3. Support future solver types
### Solver Types to Support
| Solver | Description | Status |
|--------|-------------|--------|
| `nxnastran` | NX Nastran (built-in) | Current |
| `mscnastran` | MSC Nastran (external) | Future |
| `python` | Python-based solver | Future |
| `abaqus` | Abaqus (via Python API) | Future |
| `ansys` | ANSYS (via Python API) | Future |
### Solution Types per Solver
**NX Nastran / MSC Nastran:**
- SOL101 - Linear Static
- SOL103 - Normal Modes
- SOL105 - Buckling
- SOL106 - Nonlinear Static
- SOL111 - Frequency Response
- SOL112 - Transient Response
- SOL200 - Design Optimization
**Python Solver:**
- Custom (user-defined)
### Schema Updates
#### 9.1 Update AtomizerSpec Types
```typescript
// types/atomizer-spec.ts
export type SolverEngine =
| 'nxnastran'
| 'mscnastran'
| 'python'
| 'abaqus'
| 'ansys';
export type NastranSolutionType =
| 'SOL101'
| 'SOL103'
| 'SOL105'
| 'SOL106'
| 'SOL111'
| 'SOL112'
| 'SOL200';
export interface SolverConfig {
/** Solver engine type */
engine: SolverEngine;
/** Solution type (for Nastran) */
solution_type?: NastranSolutionType;
/** Custom solver script path (for Python solver) */
script_path?: string;
/** Additional solver options */
options?: Record<string, unknown>;
}
export interface Model {
sim?: {
path: string;
solver: SolverConfig; // Changed from just 'nastran' string
};
// ...
}
```
#### 9.2 Update SolverNode Component
```typescript
// components/canvas/nodes/SolverNode.tsx
function SolverNodeComponent(props: NodeProps<SolverNodeData>) {
const { data } = props;
return (
<BaseNode {...props} icon={<Cpu size={16} />} iconColor="text-violet-400">
<div className="flex flex-col gap-1">
<span className="text-sm font-medium">{data.engine || 'nxnastran'}</span>
<span className="text-xs text-dark-400">
{data.solution_type || 'Auto-detect'}
</span>
</div>
</BaseNode>
);
}
```
#### 9.3 Solver Configuration Panel
Add to `NodeConfigPanelV2.tsx`:
```typescript
function SolverNodeConfig({ spec }: SpecConfigProps) {
const { patchSpec } = useSpecStore();
const solver = spec.model?.sim?.solver || { engine: 'nxnastran' };
const handleEngineChange = (engine: SolverEngine) => {
patchSpec('model.sim.solver.engine', engine);
};
const handleSolutionTypeChange = (type: NastranSolutionType) => {
patchSpec('model.sim.solver.solution_type', type);
};
return (
<>
<div>
<label className={labelClass}>Solver Engine</label>
<select
value={solver.engine}
onChange={(e) => handleEngineChange(e.target.value as SolverEngine)}
className={selectClass}
>
<option value="nxnastran">NX Nastran</option>
<option value="mscnastran">MSC Nastran</option>
<option value="python">Python Script</option>
</select>
</div>
{(solver.engine === 'nxnastran' || solver.engine === 'mscnastran') && (
<div>
<label className={labelClass}>Solution Type</label>
<select
value={solver.solution_type || ''}
onChange={(e) => handleSolutionTypeChange(e.target.value as NastranSolutionType)}
className={selectClass}
>
<option value="">Auto-detect from model</option>
<option value="SOL101">SOL101 - Linear Static</option>
<option value="SOL103">SOL103 - Normal Modes</option>
<option value="SOL105">SOL105 - Buckling</option>
<option value="SOL106">SOL106 - Nonlinear Static</option>
<option value="SOL111">SOL111 - Frequency Response</option>
<option value="SOL112">SOL112 - Transient Response</option>
</select>
</div>
)}
{solver.engine === 'python' && (
<div>
<label className={labelClass}>Solver Script</label>
<input
type="text"
value={solver.script_path || ''}
onChange={(e) => patchSpec('model.sim.solver.script_path', e.target.value)}
placeholder="/path/to/solver.py"
className={inputClass}
/>
<p className="text-xs text-dark-500 mt-1">
Python script that runs the analysis
</p>
</div>
)}
</>
);
}
```
#### 9.4 Files to Modify
| File | Changes |
|------|---------|
| `types/atomizer-spec.ts` | Add SolverEngine, SolverConfig types |
| `components/canvas/nodes/SolverNode.tsx` | Show engine and solution type |
| `components/canvas/panels/NodeConfigPanelV2.tsx` | Add SolverNodeConfig |
| `lib/canvas/schema.ts` | Update SolverNodeData |
| Backend: `config/spec_models.py` | Add SolverConfig Pydantic model |
---
## Implementation Order
| Phase | Effort | Priority | Dependencies |
|-------|--------|----------|--------------|
| **7.1** Resizable Panel Hook | 2h | High | None |
| **7.2** CanvasView Resizers | 2h | High | 7.1 |
| **8.1** Enable Palette Items | 1h | High | None |
| **8.2** Singleton Logic | 2h | High | 8.1 |
| **8.3** Default Node Data | 1h | High | 8.2 |
| **9.1** Schema Updates | 2h | Medium | None |
| **9.2** SolverNode UI | 1h | Medium | 9.1 |
| **9.3** Solver Config Panel | 2h | Medium | 9.1, 9.2 |
**Total Estimated Effort:** ~13 hours
---
## Success Criteria
### Phase 7 (Resizable Panels)
- [ ] Left panel can be resized between 200-400px
- [ ] Right panel can be resized between 280-600px
- [ ] Resize handles show cursor feedback
- [ ] Panel sizes persist across page reload
- [ ] Double-click on handle resets to default
### Phase 8 (Enable Palette Items)
- [ ] All 8 node types are draggable from palette
- [ ] Dragging singleton to canvas with existing node selects existing
- [ ] Toast notification explains the behavior
- [ ] New studies can start with empty canvas and add Model first
### Phase 9 (Solver Selection)
- [ ] Solver node shows engine type (nxnastran, python, etc.)
- [ ] Clicking solver node opens config panel
- [ ] Can select solver engine from dropdown
- [ ] Nastran solvers show solution type dropdown
- [ ] Python solver shows script path input
- [ ] Changes persist to atomizer_spec.json
---
## Future Considerations
### Additional Solver Support
- ANSYS integration via pyANSYS
- Abaqus integration via abaqus-python
- OpenFOAM for CFD
- Custom Python solvers with standardized interface
### Multi-Solver Workflows
- Support for chained solvers (thermal → structural)
- Co-simulation workflows
- Parallel solver execution
### Algorithm Node Enhancement
- Similar to Solver, allow algorithm selection
- Show algorithm-specific parameters
- Support custom algorithms
---
## Commit Strategy
```bash
# Phase 7
git commit -m "feat: Add resizable panels to canvas view"
# Phase 8
git commit -m "feat: Enable all palette items with singleton handling"
# Phase 9
git commit -m "feat: Add solver type selection and configuration"
```

View File

@@ -1,619 +0,0 @@
# Canvas V3 - Comprehensive Fix & Enhancement Plan
**Created**: January 16, 2026
**Status**: Planning
**Priority**: High
---
## Problem Analysis
### Critical Bugs (Must Fix)
| Issue | Impact | Root Cause (Likely) |
|-------|--------|---------------------|
| **Atomizer Assistant broken** | High - core feature unusable | WebSocket connection or chat hook error |
| **Cannot delete connections** | High - workflow editing blocked | Missing edge selection/delete handler |
| **Drag & drop positioning wrong** | Medium - UX frustration | Position calculation not accounting for scroll/zoom |
### Data Loading Issues
| Issue | Impact | Root Cause (Likely) |
|-------|--------|---------------------|
| **Auto-connect missing** | High - manual work required | `loadFromConfig` creates nodes but not edges |
| **Missing elements (OPD extractor)** | High - incomplete workflows | Incomplete parsing of `optimization_config.json` |
| **Constraints not shown** | High - incomplete workflows | Constraints not parsed from config |
| **Algorithm not pre-selected** | Medium - extra clicks | Algorithm node not created from config |
### UI/UX Issues
| Issue | Impact | Root Cause (Likely) |
|-------|--------|---------------------|
| **Interface too small** | Medium - wasted screen space | Fixed dimensions, not responsive |
| **Insufficient contrast** | Medium - accessibility | White text on light blue background |
| **Font size too small** | Low - readability | Hardcoded small font sizes |
### Feature Requests
| Feature | Value | Complexity |
|---------|-------|------------|
| **Auto-complete with Claude** | High - smart assistance | Medium |
| **Templates/guidelines** | Medium - onboarding | Low |
| **Canvas ↔ Assistant integration** | High - conversational control | High |
---
## Implementation Strategy
### Phased Approach
```
Phase 1: Critical Fixes (2 hours)
Phase 2: Data Loading (3 hours)
Phase 3: UI/UX Polish (2 hours)
Phase 4: Claude Integration (3 hours)
Phase 5: Testing & Commit (1 hour)
```
---
## Phase 1: Critical Bug Fixes
### 1.1 Fix Atomizer Assistant Error
**Investigation Steps:**
1. Check `ChatPanel.tsx` for error handling
2. Check `useCanvasChat.ts` hook for connection issues
3. Verify WebSocket endpoint `/api/chat/` is working
4. Check if `useChat.ts` base hook has errors
**Likely Fix:**
- Add error boundary around chat component
- Add null checks for WebSocket connection
- Provide fallback UI when chat unavailable
### 1.2 Enable Connection Deletion
**Current State:** Edges can't be selected or deleted
**Implementation:**
```tsx
// In AtomizerCanvas.tsx
<ReactFlow
edgesUpdatable={true}
edgesFocusable={true}
deleteKeyCode={['Backspace', 'Delete']}
onEdgeClick={(event, edge) => selectEdge(edge.id)}
/>
```
**Store Update:**
```typescript
// In useCanvasStore.ts
deleteEdge: (edgeId: string) => {
set((state) => ({
edges: state.edges.filter((e) => e.id !== edgeId),
}));
},
```
### 1.3 Fix Drag & Drop Positioning
**Problem:** New nodes appear at wrong position (not where dropped)
**Fix in AtomizerCanvas.tsx:**
```typescript
const onDrop = useCallback((event: DragEvent) => {
event.preventDefault();
if (!reactFlowInstance.current || !reactFlowWrapper.current) return;
const type = event.dataTransfer.getData('application/reactflow') as NodeType;
if (!type) return;
// Get correct position accounting for viewport transform
const bounds = reactFlowWrapper.current.getBoundingClientRect();
const position = reactFlowInstance.current.screenToFlowPosition({
x: event.clientX - bounds.left,
y: event.clientY - bounds.top,
});
addNode(type, position);
}, [addNode]);
```
---
## Phase 2: Data Loading Improvements
### 2.1 Enhanced `loadFromConfig` Function
**Goal:** When loading a study, create ALL nodes AND edges automatically.
**Current Problems:**
- Nodes created but not connected
- Some extractors/constraints missing
- Algorithm not created
**New Implementation Strategy:**
```typescript
loadFromConfig: (config: OptimizationConfig) => {
const nodes: Node[] = [];
const edges: Edge[] = [];
// Layout constants
const COLS = { model: 50, dvar: 50, solver: 250, extractor: 450, obj: 650, algo: 850 };
const ROW_HEIGHT = 100;
const START_Y = 100;
// Track IDs for connections
const nodeIds = {
model: '',
solver: '',
dvars: [] as string[],
extractors: {} as Record<string, string>, // extractor_id -> node_id
objectives: [] as string[],
constraints: [] as string[],
algorithm: '',
};
// 1. Create Model Node
if (config.nx_model) {
const id = `model_${Date.now()}`;
nodeIds.model = id;
nodes.push({
id,
type: 'model',
position: { x: COLS.model, y: START_Y },
data: {
type: 'model',
label: 'Model',
configured: true,
filePath: config.nx_model.sim_path || config.nx_model.prt_path,
fileType: config.nx_model.sim_path ? 'sim' : 'prt',
},
});
}
// 2. Create Solver Node
if (config.solver) {
const id = `solver_${Date.now()}`;
nodeIds.solver = id;
nodes.push({
id,
type: 'solver',
position: { x: COLS.solver, y: START_Y },
data: {
type: 'solver',
label: 'Solver',
configured: true,
solverType: `SOL${config.solver.solution_type}`,
},
});
// Connect Model → Solver
if (nodeIds.model) {
edges.push({
id: `e_model_solver`,
source: nodeIds.model,
target: nodeIds.solver,
animated: true,
});
}
}
// 3. Create Design Variables (connected to Model)
config.design_variables?.forEach((dv, i) => {
const id = `dvar_${i}_${Date.now()}`;
nodeIds.dvars.push(id);
nodes.push({
id,
type: 'designVar',
position: { x: COLS.dvar, y: START_Y + 150 + i * ROW_HEIGHT },
data: {
type: 'designVar',
label: dv.name,
configured: true,
expressionName: dv.nx_expression || dv.name,
minValue: dv.lower_bound,
maxValue: dv.upper_bound,
unit: dv.unit,
},
});
// Connect DVar → Model (or keep disconnected, they're inputs)
});
// 4. Create Extractors from objectives AND constraints
const allExtractors = new Set<string>();
config.objectives?.forEach(obj => allExtractors.add(obj.extractor_id));
config.constraints?.forEach(con => {
if (con.extractor_id) allExtractors.add(con.extractor_id);
});
let extractorRow = 0;
allExtractors.forEach((extractorId) => {
const id = `extractor_${extractorId}_${Date.now()}`;
nodeIds.extractors[extractorId] = id;
// Find extractor config
const objWithExt = config.objectives?.find(o => o.extractor_id === extractorId);
const conWithExt = config.constraints?.find(c => c.extractor_id === extractorId);
nodes.push({
id,
type: 'extractor',
position: { x: COLS.extractor, y: START_Y + extractorRow * ROW_HEIGHT },
data: {
type: 'extractor',
label: extractorId,
configured: true,
extractorId: extractorId,
extractorName: objWithExt?.name || conWithExt?.name || extractorId,
},
});
extractorRow++;
// Connect Solver → Extractor
if (nodeIds.solver) {
edges.push({
id: `e_solver_${extractorId}`,
source: nodeIds.solver,
target: id,
});
}
});
// 5. Create Objectives (connected to Extractors)
config.objectives?.forEach((obj, i) => {
const id = `obj_${i}_${Date.now()}`;
nodeIds.objectives.push(id);
nodes.push({
id,
type: 'objective',
position: { x: COLS.obj, y: START_Y + i * ROW_HEIGHT },
data: {
type: 'objective',
label: obj.name,
configured: true,
name: obj.name,
direction: obj.direction,
weight: obj.weight,
},
});
// Connect Extractor → Objective
const extractorNodeId = nodeIds.extractors[obj.extractor_id];
if (extractorNodeId) {
edges.push({
id: `e_ext_obj_${i}`,
source: extractorNodeId,
target: id,
});
}
});
// 6. Create Constraints (connected to Extractors)
config.constraints?.forEach((con, i) => {
const id = `con_${i}_${Date.now()}`;
nodeIds.constraints.push(id);
const objY = START_Y + (config.objectives?.length || 0) * ROW_HEIGHT;
nodes.push({
id,
type: 'constraint',
position: { x: COLS.obj, y: objY + i * ROW_HEIGHT },
data: {
type: 'constraint',
label: con.name,
configured: true,
name: con.name,
operator: con.type === 'upper' ? '<=' : con.type === 'lower' ? '>=' : '==',
value: con.upper_bound ?? con.lower_bound ?? con.target,
},
});
// Connect Extractor → Constraint
if (con.extractor_id) {
const extractorNodeId = nodeIds.extractors[con.extractor_id];
if (extractorNodeId) {
edges.push({
id: `e_ext_con_${i}`,
source: extractorNodeId,
target: id,
});
}
}
});
// 7. Create Algorithm Node
if (config.optimization) {
const id = `algo_${Date.now()}`;
nodeIds.algorithm = id;
nodes.push({
id,
type: 'algorithm',
position: { x: COLS.algo, y: START_Y },
data: {
type: 'algorithm',
label: 'Algorithm',
configured: true,
method: config.optimization.sampler || 'TPE',
maxTrials: config.optimization.n_trials || 100,
},
});
// Connect Objectives → Algorithm
nodeIds.objectives.forEach((objId, i) => {
edges.push({
id: `e_obj_algo_${i}`,
source: objId,
target: id,
});
});
}
// 8. Create Surrogate Node (if enabled)
if (config.surrogate?.enabled) {
const id = `surrogate_${Date.now()}`;
nodes.push({
id,
type: 'surrogate',
position: { x: COLS.algo, y: START_Y + 150 },
data: {
type: 'surrogate',
label: 'Surrogate',
configured: true,
enabled: true,
modelType: config.surrogate.type || 'MLP',
minTrials: config.surrogate.min_trials || 20,
},
});
}
// Apply to store
set({
nodes,
edges,
selectedNode: null,
validation: { valid: false, errors: [], warnings: [] },
});
};
```
### 2.2 Parse Full Config Structure
**Ensure we handle:**
- `nx_model.sim_path` / `prt_path` / `fem_path`
- `solver.solution_type`
- `design_variables[]` with all fields
- `objectives[]` with `extractor_id`, `name`, `direction`, `weight`
- `constraints[]` with `extractor_id`, `type`, `upper_bound`, `lower_bound`
- `optimization.sampler`, `n_trials`
- `surrogate.enabled`, `type`, `min_trials`
- `post_processing[]` (for future)
---
## Phase 3: UI/UX Polish
### 3.1 Responsive Full-Screen Canvas
**CanvasView.tsx:**
```tsx
export function CanvasView() {
return (
<div className="h-screen w-screen flex flex-col overflow-hidden">
{/* Minimal header */}
<header className="flex-shrink-0 h-10 bg-dark-900 border-b border-dark-700 px-4 flex items-center justify-between">
<span className="text-sm font-medium text-white">Canvas Builder</span>
<div className="flex gap-2">
<button>Templates</button>
<button>Import</button>
<button>Clear</button>
</div>
</header>
{/* Canvas fills remaining space */}
<main className="flex-1 min-h-0">
<AtomizerCanvas />
</main>
</div>
);
}
```
### 3.2 Fix Contrast Issues
**Problem:** White text on light blue is hard to read
**Solution:** Use darker backgrounds or different text colors
```css
/* Node backgrounds */
.bg-dark-850 /* #0A1525 - dark enough for white text */
/* Avoid light blue backgrounds with white text */
/* If using blue, use dark blue (#1e3a5f) or switch to light text */
/* Specific fixes */
.node-header {
background: #0F1E32; /* dark-800 */
color: #FFFFFF;
}
.node-content {
background: #0A1525; /* dark-850 */
color: #E2E8F0; /* light gray */
}
/* Badge/pill text */
.badge-primary {
background: rgba(0, 212, 230, 0.15); /* primary with low opacity */
color: #00D4E6; /* primary-400 */
border: 1px solid rgba(0, 212, 230, 0.3);
}
```
### 3.3 Increase Font Sizes
**Current vs New:**
| Element | Current | New |
|---------|---------|-----|
| Node label | 12px | 14px |
| Node detail | 10px | 12px |
| Palette item | 12px | 14px |
| Panel headers | 14px | 16px |
| Config labels | 10px | 12px |
---
## Phase 4: Claude Integration
### 4.1 Fix Chat Panel Connection
**Error Handling:**
```tsx
function ChatPanel({ onClose }: ChatPanelProps) {
const { messages, isConnected, isThinking, error } = useCanvasChat();
if (error) {
return (
<div className="flex flex-col items-center justify-center h-full p-4">
<AlertCircle className="text-red-400 mb-2" size={24} />
<p className="text-red-400 text-sm text-center">{error}</p>
<button
onClick={reconnect}
className="mt-4 px-3 py-1.5 bg-dark-700 rounded text-sm"
>
Retry Connection
</button>
</div>
);
}
// ... rest of component
}
```
### 4.2 Auto-Complete with Claude
**Concept:** A button that sends current canvas state to Claude and asks for suggestions.
**UI:**
- Button: "Complete with Claude" next to Validate
- Opens chat panel with Claude's analysis
- Claude suggests: missing nodes, connections, configuration improvements
**Implementation:**
```typescript
// In useCanvasChat.ts
const autoCompleteWithClaude = useCallback(async () => {
const intent = toIntent();
const message = `Analyze this Canvas workflow and suggest what's missing or could be improved:
\`\`\`json
${JSON.stringify(intent, null, 2)}
\`\`\`
Please:
1. Identify any missing required components
2. Suggest extractors that should be added based on the objectives
3. Recommend connections that should be made
4. Propose any configuration improvements
Be specific and actionable.`;
await sendMessage(message);
}, [toIntent, sendMessage]);
```
### 4.3 Canvas ↔ Assistant Integration (Future)
**Vision:** Claude can modify the canvas through conversation.
**Commands:**
- "Add a displacement extractor"
- "Connect the mass extractor to objective 1"
- "Set the algorithm to CMA-ES with 200 trials"
- "Load the bracket_v3 study"
**Implementation Approach:**
1. Define canvas manipulation actions as Claude tools
2. Parse Claude responses for action intents
3. Execute actions via store methods
**MCP Tools (New):**
- `canvas_add_node` - Add a node of specified type
- `canvas_remove_node` - Remove a node by ID
- `canvas_connect` - Connect two nodes
- `canvas_disconnect` - Remove a connection
- `canvas_configure` - Update node configuration
- `canvas_load_study` - Load a study into canvas
---
## Phase 5: Testing & Validation
### Test Cases
| Test | Steps | Expected |
|------|-------|----------|
| Load study with connections | Import → Select study → Load | All nodes + edges appear |
| Delete connection | Click edge → Press Delete | Edge removed |
| Drag & drop position | Drag node to specific spot | Node appears at drop location |
| Chat panel opens | Click chat icon | Panel opens without error |
| Full screen canvas | Open /canvas | Canvas fills window |
| Contrast readable | View all nodes | All text legible |
### Build Verification
```bash
cd atomizer-dashboard/frontend
npm run build
# Must pass without errors
```
---
## Files to Modify
| File | Changes |
|------|---------|
| `useCanvasStore.ts` | Enhanced `loadFromConfig`, `deleteEdge` |
| `AtomizerCanvas.tsx` | Edge deletion, drag/drop fix, responsive |
| `CanvasView.tsx` | Full-screen layout |
| `ChatPanel.tsx` | Error handling, reconnect |
| `useCanvasChat.ts` | Auto-complete function, error state |
| `BaseNode.tsx` | Font sizes, contrast |
| `NodePalette.tsx` | Font sizes |
| `NodeConfigPanel.tsx` | Font sizes, contrast |
---
## Summary
**Total Effort:** ~11 hours across 5 phases
**Priority Order:**
1. Fix Atomizer Assistant (blocking)
2. Fix connection deletion (blocking editing)
3. Fix data loading (core functionality)
4. UI/UX polish (user experience)
5. Claude integration (enhancement)
**Success Criteria:**
- [ ] All bugs from user report fixed
- [ ] Loading a study shows ALL elements with connections
- [ ] Canvas is responsive and readable
- [ ] Chat panel works without errors
- [ ] Build passes without errors
---
*Plan created for Ralph Loop autonomous execution.*

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff