## Documentation Updates - DASHBOARD.md: Updated to V3.0 with Canvas V3 features, file browser, introspection - DASHBOARD_IMPLEMENTATION_STATUS.md: Marked Canvas V3 features as COMPLETE - CANVAS.md: New comprehensive guide for Canvas Builder V3 with all features - CLAUDE.md: Added dashboard quick reference and Canvas V3 features ## Canvas V3 Features Documented - File Browser: Browse studies directory for model files - Model Introspection: Auto-discover expressions, solver type, dependencies - One-Click Add: Add expressions as design variables instantly - Claude Bug Fixes: WebSocket reconnection, SQL errors resolved - Health Check: /api/health endpoint for monitoring ## Backend Services - NX introspection service with expression discovery - File browser API with type filtering - Claude session management improvements - Context builder enhancements ## Frontend Components - FileBrowser: Modal for file selection with search - IntrospectionPanel: View discovered model information - ExpressionSelector: Dropdown for design variable configuration - Improved chat hooks with reconnection logic ## Plan Documents - Added RALPH_LOOP_CANVAS_V2/V3 implementation records - Added ATOMIZER_DASHBOARD_V2_MASTER_PLAN - Added investigation and sync documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1252 lines
41 KiB
Markdown
1252 lines
41 KiB
Markdown
# Ralph Loop: Canvas V3 - Complete Fix & UX Enhancement
|
|
|
|
**Purpose:** Fix all Canvas bugs AND implement proper study management workflow
|
|
**Execution:** Autonomous, all phases sequential, no stopping
|
|
**Estimated Duration:** 15 work units
|
|
|
|
---
|
|
|
|
## Launch Command
|
|
|
|
```powershell
|
|
cd C:\Users\antoi\Atomizer
|
|
claude --dangerously-skip-permissions
|
|
```
|
|
|
|
Paste everything below the line.
|
|
|
|
---
|
|
|
|
You are executing a **multi-phase autonomous development session** to fix and enhance the Atomizer Canvas Builder with proper study management.
|
|
|
|
## Problem Summary
|
|
|
|
### Bugs to Fix
|
|
1. **Atomizer Assistant broken** - Error when opening chat panel
|
|
2. **Cannot delete connections** - No way to remove edges
|
|
3. **Drag & drop wrong position** - Nodes appear at wrong location
|
|
4. **Auto-connect missing** - Loading study doesn't create edges
|
|
5. **Missing elements on load** - Extractors, constraints not loaded
|
|
6. **Algorithm not pre-selected** - Algorithm node missing from load
|
|
7. **Interface too small** - Canvas not using full screen
|
|
8. **Contrast issues** - White on blue hard to read
|
|
|
|
### Features to Add
|
|
1. **Study context awareness** - Know which study is active
|
|
2. **New Study workflow** - Create studies from scratch
|
|
3. **Process dialog** - Choose update vs. create new study
|
|
4. **Home page study browser** - List all studies, quick actions
|
|
5. **Context-aware Canvas header** - Show current study name
|
|
|
|
## Environment
|
|
|
|
```
|
|
Working Directory: C:/Users/antoi/Atomizer
|
|
Frontend: atomizer-dashboard/frontend/
|
|
Backend: atomizer-dashboard/backend/
|
|
Python: C:/Users/antoi/anaconda3/envs/atomizer/python.exe
|
|
Git: Push to origin AND github
|
|
```
|
|
|
|
## Execution Rules
|
|
|
|
1. **TodoWrite** - Track every task, mark complete immediately
|
|
2. **Sequential phases** - Complete each phase fully before next
|
|
3. **Test after each phase** - Run `npm run build` to verify
|
|
4. **No questions** - Make reasonable decisions based on codebase
|
|
5. **Commit per phase** - Git commit after each major phase
|
|
6. **Read before edit** - Always read files before modifying
|
|
|
|
---
|
|
|
|
# PHASE 1: Critical Bug Fixes (Same as before)
|
|
|
|
## T1.1 - Fix Atomizer Assistant Error
|
|
|
|
Read and fix:
|
|
```
|
|
Read: atomizer-dashboard/frontend/src/components/canvas/panels/ChatPanel.tsx
|
|
Read: atomizer-dashboard/frontend/src/hooks/useCanvasChat.ts
|
|
```
|
|
|
|
Add error handling with reconnect button. Wrap in try/catch.
|
|
|
|
## T1.2 - Enable Connection Deletion
|
|
|
|
In `AtomizerCanvas.tsx`:
|
|
- Add `onEdgeClick` handler to select edge
|
|
- Add keyboard listener for Delete/Backspace
|
|
- Add `selectedEdge` state
|
|
|
|
In `useCanvasStore.ts`:
|
|
- Add `deleteEdge(edgeId)` action
|
|
|
|
## T1.3 - Fix Drag & Drop Positioning
|
|
|
|
Fix `onDrop` in AtomizerCanvas.tsx to use `screenToFlowPosition` correctly with screen coordinates.
|
|
|
|
---
|
|
|
|
# PHASE 2: Complete Data Loading Fix
|
|
|
|
## T2.1 - Rewrite loadFromConfig
|
|
|
|
Complete rewrite of `useCanvasStore.ts` `loadFromConfig` to:
|
|
1. Create ALL node types (model, solver, dvars, extractors, objectives, constraints, algorithm, surrogate)
|
|
2. Create ALL edges (model→solver, solver→extractors, extractors→objectives, extractors→constraints, objectives→algorithm)
|
|
3. Proper column layout (model/dvar: 50, solver: 250, extractor: 450, obj/con: 650, algo: 850)
|
|
4. Parse all config fields including constraints and optimization.sampler
|
|
|
|
---
|
|
|
|
# PHASE 3: UI/UX Polish
|
|
|
|
## T3.1 - Full-Screen Responsive Canvas
|
|
|
|
Update `CanvasView.tsx` with minimal header and flex-1 canvas area.
|
|
|
|
## T3.2 - Fix Contrast
|
|
|
|
Ensure all nodes use `bg-dark-850` with white/light text. No white on light blue.
|
|
|
|
## T3.3 - Increase Font Sizes
|
|
|
|
Update BaseNode, NodePalette, NodeConfigPanel for larger, readable fonts.
|
|
|
|
---
|
|
|
|
# PHASE 4: Study Context System (NEW)
|
|
|
|
## T4.1 - Create Study Context
|
|
|
|
**File:** `atomizer-dashboard/frontend/src/contexts/StudyContext.tsx`
|
|
|
|
```tsx
|
|
import React, { createContext, useContext, useState, useCallback, useEffect } from 'react';
|
|
|
|
interface Study {
|
|
path: string;
|
|
name: string;
|
|
category: string;
|
|
status: 'not_started' | 'running' | 'paused' | 'completed';
|
|
trialCount: number;
|
|
maxTrials: number;
|
|
bestValue?: number;
|
|
hasConfig: boolean;
|
|
}
|
|
|
|
interface StudyContextState {
|
|
currentStudy: Study | null;
|
|
isCreatingNew: boolean;
|
|
pendingStudyName: string | null;
|
|
pendingCategory: string | null;
|
|
studies: Study[];
|
|
categories: string[];
|
|
isLoading: boolean;
|
|
|
|
selectStudy: (studyPath: string) => Promise<void>;
|
|
startNewStudy: (name: string, category: string) => void;
|
|
confirmNewStudy: () => Promise<Study>;
|
|
cancelNewStudy: () => void;
|
|
closeStudy: () => void;
|
|
refreshStudies: () => Promise<void>;
|
|
}
|
|
|
|
const StudyContext = createContext<StudyContextState | null>(null);
|
|
|
|
export function StudyProvider({ children }: { children: React.ReactNode }) {
|
|
const [currentStudy, setCurrentStudy] = useState<Study | null>(null);
|
|
const [isCreatingNew, setIsCreatingNew] = useState(false);
|
|
const [pendingStudyName, setPendingStudyName] = useState<string | null>(null);
|
|
const [pendingCategory, setPendingCategory] = useState<string | null>(null);
|
|
const [studies, setStudies] = useState<Study[]>([]);
|
|
const [categories, setCategories] = useState<string[]>([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
const refreshStudies = useCallback(async () => {
|
|
setIsLoading(true);
|
|
try {
|
|
const res = await fetch('/api/studies');
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
setStudies(data.studies || []);
|
|
setCategories(data.categories || []);
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to fetch studies:', err);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
refreshStudies();
|
|
}, [refreshStudies]);
|
|
|
|
const selectStudy = useCallback(async (studyPath: string) => {
|
|
try {
|
|
const res = await fetch(`/api/studies/${encodeURIComponent(studyPath)}`);
|
|
if (res.ok) {
|
|
const study = await res.json();
|
|
setCurrentStudy(study);
|
|
setIsCreatingNew(false);
|
|
setPendingStudyName(null);
|
|
setPendingCategory(null);
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to select study:', err);
|
|
}
|
|
}, []);
|
|
|
|
const startNewStudy = useCallback((name: string, category: string) => {
|
|
setIsCreatingNew(true);
|
|
setPendingStudyName(name);
|
|
setPendingCategory(category);
|
|
setCurrentStudy(null);
|
|
}, []);
|
|
|
|
const confirmNewStudy = useCallback(async (): Promise<Study> => {
|
|
if (!pendingStudyName || !pendingCategory) {
|
|
throw new Error('No pending study to confirm');
|
|
}
|
|
|
|
const res = await fetch('/api/studies', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
name: pendingStudyName,
|
|
category: pendingCategory,
|
|
}),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
throw new Error('Failed to create study');
|
|
}
|
|
|
|
const newStudy = await res.json();
|
|
setCurrentStudy(newStudy);
|
|
setIsCreatingNew(false);
|
|
setPendingStudyName(null);
|
|
setPendingCategory(null);
|
|
await refreshStudies();
|
|
return newStudy;
|
|
}, [pendingStudyName, pendingCategory, refreshStudies]);
|
|
|
|
const cancelNewStudy = useCallback(() => {
|
|
setIsCreatingNew(false);
|
|
setPendingStudyName(null);
|
|
setPendingCategory(null);
|
|
}, []);
|
|
|
|
const closeStudy = useCallback(() => {
|
|
setCurrentStudy(null);
|
|
setIsCreatingNew(false);
|
|
setPendingStudyName(null);
|
|
setPendingCategory(null);
|
|
}, []);
|
|
|
|
return (
|
|
<StudyContext.Provider
|
|
value={{
|
|
currentStudy,
|
|
isCreatingNew,
|
|
pendingStudyName,
|
|
pendingCategory,
|
|
studies,
|
|
categories,
|
|
isLoading,
|
|
selectStudy,
|
|
startNewStudy,
|
|
confirmNewStudy,
|
|
cancelNewStudy,
|
|
closeStudy,
|
|
refreshStudies,
|
|
}}
|
|
>
|
|
{children}
|
|
</StudyContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useStudyContext() {
|
|
const ctx = useContext(StudyContext);
|
|
if (!ctx) throw new Error('useStudyContext must be used within StudyProvider');
|
|
return ctx;
|
|
}
|
|
```
|
|
|
|
## T4.2 - Update App.tsx to Include Provider
|
|
|
|
```tsx
|
|
import { StudyProvider } from './contexts/StudyContext';
|
|
|
|
function App() {
|
|
return (
|
|
<StudyProvider>
|
|
{/* ... rest of app */}
|
|
</StudyProvider>
|
|
);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
# PHASE 5: Home Page Study Browser (NEW)
|
|
|
|
## T5.1 - Create Study Browser Component
|
|
|
|
**File:** `atomizer-dashboard/frontend/src/components/StudyBrowser.tsx`
|
|
|
|
```tsx
|
|
import { useState } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { useStudyContext } from '../contexts/StudyContext';
|
|
import {
|
|
FolderOpen,
|
|
Play,
|
|
Pause,
|
|
CheckCircle,
|
|
Circle,
|
|
Plus,
|
|
Search,
|
|
LayoutDashboard,
|
|
FileBarChart,
|
|
Workflow
|
|
} from 'lucide-react';
|
|
|
|
export function StudyBrowser() {
|
|
const navigate = useNavigate();
|
|
const { studies, categories, isLoading, selectStudy, refreshStudies } = useStudyContext();
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
|
const [showNewStudyDialog, setShowNewStudyDialog] = useState(false);
|
|
|
|
const filteredStudies = studies.filter((study) => {
|
|
const matchesSearch = study.name.toLowerCase().includes(searchTerm.toLowerCase());
|
|
const matchesCategory = !selectedCategory || study.category === selectedCategory;
|
|
return matchesSearch && matchesCategory;
|
|
});
|
|
|
|
const getStatusIcon = (status: string) => {
|
|
switch (status) {
|
|
case 'running':
|
|
return <Play size={14} className="text-green-400 fill-green-400" />;
|
|
case 'paused':
|
|
return <Pause size={14} className="text-amber-400" />;
|
|
case 'completed':
|
|
return <CheckCircle size={14} className="text-blue-400" />;
|
|
default:
|
|
return <Circle size={14} className="text-dark-500" />;
|
|
}
|
|
};
|
|
|
|
const handleStudyAction = async (study: any, action: 'dashboard' | 'canvas' | 'results') => {
|
|
await selectStudy(study.path);
|
|
navigate(`/${action}`);
|
|
};
|
|
|
|
return (
|
|
<div className="h-full flex flex-col bg-dark-900 p-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h1 className="text-2xl font-bold text-white">Studies</h1>
|
|
<button
|
|
onClick={() => setShowNewStudyDialog(true)}
|
|
className="flex items-center gap-2 px-4 py-2 bg-primary-500 hover:bg-primary-600
|
|
text-white rounded-lg font-medium transition-colors"
|
|
>
|
|
<Plus size={18} />
|
|
New Study
|
|
</button>
|
|
</div>
|
|
|
|
{/* Search & Filter */}
|
|
<div className="flex items-center gap-4 mb-6">
|
|
<div className="relative flex-1 max-w-md">
|
|
<Search size={18} className="absolute left-3 top-1/2 -translate-y-1/2 text-dark-500" />
|
|
<input
|
|
type="text"
|
|
placeholder="Search studies..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="w-full pl-10 pr-4 py-2 bg-dark-800 border border-dark-700 rounded-lg
|
|
text-white placeholder-dark-500 focus:outline-none focus:border-primary-500"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={() => setSelectedCategory(null)}
|
|
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors
|
|
${!selectedCategory ? 'bg-primary-500/20 text-primary-400' : 'text-dark-400 hover:text-white'}`}
|
|
>
|
|
All
|
|
</button>
|
|
{categories.map((cat) => (
|
|
<button
|
|
key={cat}
|
|
onClick={() => setSelectedCategory(cat)}
|
|
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors
|
|
${selectedCategory === cat ? 'bg-primary-500/20 text-primary-400' : 'text-dark-400 hover:text-white'}`}
|
|
>
|
|
{cat}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Study List */}
|
|
<div className="flex-1 overflow-auto">
|
|
{isLoading ? (
|
|
<div className="flex items-center justify-center h-32 text-dark-500">
|
|
Loading studies...
|
|
</div>
|
|
) : filteredStudies.length === 0 ? (
|
|
<div className="flex flex-col items-center justify-center h-32 text-dark-500">
|
|
<FolderOpen size={32} className="mb-2" />
|
|
<p>No studies found</p>
|
|
</div>
|
|
) : (
|
|
<div className="grid gap-3">
|
|
{filteredStudies.map((study) => (
|
|
<div
|
|
key={study.path}
|
|
className="bg-dark-800 border border-dark-700 rounded-xl p-4
|
|
hover:border-dark-600 transition-colors"
|
|
>
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex items-center gap-3">
|
|
{getStatusIcon(study.status)}
|
|
<div>
|
|
<h3 className="font-semibold text-white">{study.name}</h3>
|
|
<p className="text-sm text-dark-400">
|
|
{study.category} • {study.trialCount}/{study.maxTrials} trials
|
|
{study.bestValue !== undefined && ` • Best: ${study.bestValue.toFixed(2)}`}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1">
|
|
<button
|
|
onClick={() => handleStudyAction(study, 'dashboard')}
|
|
className="p-2 text-dark-400 hover:text-white hover:bg-dark-700 rounded-lg"
|
|
title="Dashboard"
|
|
>
|
|
<LayoutDashboard size={18} />
|
|
</button>
|
|
<button
|
|
onClick={() => handleStudyAction(study, 'canvas')}
|
|
className="p-2 text-dark-400 hover:text-white hover:bg-dark-700 rounded-lg"
|
|
title="Canvas"
|
|
>
|
|
<Workflow size={18} />
|
|
</button>
|
|
<button
|
|
onClick={() => handleStudyAction(study, 'results')}
|
|
className="p-2 text-dark-400 hover:text-white hover:bg-dark-700 rounded-lg"
|
|
title="Results"
|
|
>
|
|
<FileBarChart size={18} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* New Study Dialog */}
|
|
{showNewStudyDialog && (
|
|
<NewStudyDialog onClose={() => setShowNewStudyDialog(false)} />
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
## T5.2 - Create New Study Dialog
|
|
|
|
**File:** `atomizer-dashboard/frontend/src/components/NewStudyDialog.tsx`
|
|
|
|
```tsx
|
|
import { useState } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { useStudyContext } from '../contexts/StudyContext';
|
|
import { X, LayoutTemplate, Copy, FileBox } from 'lucide-react';
|
|
|
|
interface Props {
|
|
onClose: () => void;
|
|
}
|
|
|
|
export function NewStudyDialog({ onClose }: Props) {
|
|
const navigate = useNavigate();
|
|
const { categories, studies, startNewStudy } = useStudyContext();
|
|
|
|
const [name, setName] = useState('');
|
|
const [category, setCategory] = useState(categories[0] || 'General');
|
|
const [newCategory, setNewCategory] = useState('');
|
|
const [startWith, setStartWith] = useState<'blank' | 'template' | 'copy'>('blank');
|
|
const [selectedTemplate, setSelectedTemplate] = useState('mass_minimization');
|
|
const [copyFrom, setCopyFrom] = useState('');
|
|
|
|
const handleCreate = () => {
|
|
const finalCategory = newCategory || category;
|
|
startNewStudy(name, finalCategory);
|
|
// Navigate to canvas with the starting mode
|
|
navigate('/canvas', {
|
|
state: {
|
|
newStudy: true,
|
|
startWith,
|
|
template: startWith === 'template' ? selectedTemplate : null,
|
|
copyFrom: startWith === 'copy' ? copyFrom : null,
|
|
},
|
|
});
|
|
onClose();
|
|
};
|
|
|
|
const isValid = name.trim().length > 0 && (category || newCategory);
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
|
|
<div className="bg-dark-850 border border-dark-700 rounded-xl w-full max-w-lg shadow-2xl">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between px-6 py-4 border-b border-dark-700">
|
|
<h2 className="text-lg font-semibold text-white">Create New Study</h2>
|
|
<button onClick={onClose} className="p-1 text-dark-400 hover:text-white">
|
|
<X size={20} />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Body */}
|
|
<div className="px-6 py-4 space-y-4">
|
|
{/* Study Name */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-dark-300 mb-1.5">
|
|
Study Name
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value.replace(/[^a-zA-Z0-9_-]/g, '_'))}
|
|
placeholder="my_new_study"
|
|
className="w-full px-3 py-2 bg-dark-800 border border-dark-600 rounded-lg
|
|
text-white placeholder-dark-500 focus:outline-none focus:border-primary-500"
|
|
/>
|
|
<p className="text-xs text-dark-500 mt-1">Use snake_case (letters, numbers, underscores)</p>
|
|
</div>
|
|
|
|
{/* Category */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-dark-300 mb-1.5">
|
|
Category
|
|
</label>
|
|
<div className="flex gap-2">
|
|
<select
|
|
value={category}
|
|
onChange={(e) => {
|
|
setCategory(e.target.value);
|
|
setNewCategory('');
|
|
}}
|
|
className="flex-1 px-3 py-2 bg-dark-800 border border-dark-600 rounded-lg
|
|
text-white focus:outline-none focus:border-primary-500"
|
|
>
|
|
{categories.map((cat) => (
|
|
<option key={cat} value={cat}>{cat}</option>
|
|
))}
|
|
<option value="">+ New Category</option>
|
|
</select>
|
|
{category === '' && (
|
|
<input
|
|
type="text"
|
|
value={newCategory}
|
|
onChange={(e) => setNewCategory(e.target.value)}
|
|
placeholder="New category name"
|
|
className="flex-1 px-3 py-2 bg-dark-800 border border-dark-600 rounded-lg
|
|
text-white placeholder-dark-500 focus:outline-none focus:border-primary-500"
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Start With */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-dark-300 mb-2">
|
|
Start With
|
|
</label>
|
|
<div className="space-y-2">
|
|
{/* Blank */}
|
|
<label className="flex items-center gap-3 p-3 bg-dark-800 border border-dark-600
|
|
rounded-lg cursor-pointer hover:border-dark-500">
|
|
<input
|
|
type="radio"
|
|
name="startWith"
|
|
value="blank"
|
|
checked={startWith === 'blank'}
|
|
onChange={() => setStartWith('blank')}
|
|
className="text-primary-500"
|
|
/>
|
|
<FileBox size={20} className="text-dark-400" />
|
|
<div>
|
|
<p className="font-medium text-white">Blank Canvas</p>
|
|
<p className="text-xs text-dark-500">Start from scratch</p>
|
|
</div>
|
|
</label>
|
|
|
|
{/* Template */}
|
|
<label className="flex items-center gap-3 p-3 bg-dark-800 border border-dark-600
|
|
rounded-lg cursor-pointer hover:border-dark-500">
|
|
<input
|
|
type="radio"
|
|
name="startWith"
|
|
value="template"
|
|
checked={startWith === 'template'}
|
|
onChange={() => setStartWith('template')}
|
|
className="text-primary-500"
|
|
/>
|
|
<LayoutTemplate size={20} className="text-dark-400" />
|
|
<div className="flex-1">
|
|
<p className="font-medium text-white">From Template</p>
|
|
{startWith === 'template' && (
|
|
<select
|
|
value={selectedTemplate}
|
|
onChange={(e) => setSelectedTemplate(e.target.value)}
|
|
className="mt-1 w-full px-2 py-1 bg-dark-700 border border-dark-600 rounded
|
|
text-sm text-white"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<option value="mass_minimization">Mass Minimization</option>
|
|
<option value="multi_objective">Multi-Objective</option>
|
|
<option value="frequency_target">Frequency Target</option>
|
|
<option value="turbo_mode">Turbo Mode</option>
|
|
<option value="mirror_wfe">Mirror WFE</option>
|
|
</select>
|
|
)}
|
|
</div>
|
|
</label>
|
|
|
|
{/* Copy From */}
|
|
<label className="flex items-center gap-3 p-3 bg-dark-800 border border-dark-600
|
|
rounded-lg cursor-pointer hover:border-dark-500">
|
|
<input
|
|
type="radio"
|
|
name="startWith"
|
|
value="copy"
|
|
checked={startWith === 'copy'}
|
|
onChange={() => setStartWith('copy')}
|
|
className="text-primary-500"
|
|
/>
|
|
<Copy size={20} className="text-dark-400" />
|
|
<div className="flex-1">
|
|
<p className="font-medium text-white">Copy from Existing</p>
|
|
{startWith === 'copy' && (
|
|
<select
|
|
value={copyFrom}
|
|
onChange={(e) => setCopyFrom(e.target.value)}
|
|
className="mt-1 w-full px-2 py-1 bg-dark-700 border border-dark-600 rounded
|
|
text-sm text-white"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<option value="">Select a study...</option>
|
|
{studies.filter(s => s.hasConfig).map((s) => (
|
|
<option key={s.path} value={s.path}>{s.name}</option>
|
|
))}
|
|
</select>
|
|
)}
|
|
</div>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="flex items-center justify-end gap-3 px-6 py-4 border-t border-dark-700">
|
|
<button
|
|
onClick={onClose}
|
|
className="px-4 py-2 text-dark-300 hover:text-white transition-colors"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
onClick={handleCreate}
|
|
disabled={!isValid}
|
|
className="px-4 py-2 bg-primary-500 hover:bg-primary-600 disabled:bg-dark-700
|
|
disabled:text-dark-500 text-white rounded-lg font-medium transition-colors"
|
|
>
|
|
Create & Open Canvas
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
# PHASE 6: Process Dialog with Update/Create Choice (NEW)
|
|
|
|
## T6.1 - Create Process Dialog Component
|
|
|
|
**File:** `atomizer-dashboard/frontend/src/components/canvas/panels/ProcessDialog.tsx`
|
|
|
|
```tsx
|
|
import { useState } from 'react';
|
|
import { useStudyContext } from '../../../contexts/StudyContext';
|
|
import { X, AlertTriangle, FolderPlus, RefreshCw, Loader2 } from 'lucide-react';
|
|
|
|
interface Props {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onProcess: (options: ProcessOptions) => void;
|
|
isProcessing: boolean;
|
|
}
|
|
|
|
export interface ProcessOptions {
|
|
mode: 'update' | 'create';
|
|
studyName?: string;
|
|
category?: string;
|
|
copyModelFiles?: boolean;
|
|
openAfter?: boolean;
|
|
runAfter?: boolean;
|
|
}
|
|
|
|
export function ProcessDialog({ isOpen, onClose, onProcess, isProcessing }: Props) {
|
|
const { currentStudy, isCreatingNew, pendingStudyName, pendingCategory, categories } = useStudyContext();
|
|
|
|
const [mode, setMode] = useState<'update' | 'create'>(isCreatingNew ? 'create' : 'update');
|
|
const [newStudyName, setNewStudyName] = useState('');
|
|
const [newCategory, setNewCategory] = useState(currentStudy?.category || categories[0] || 'General');
|
|
const [copyModelFiles, setCopyModelFiles] = useState(true);
|
|
const [openAfter, setOpenAfter] = useState(true);
|
|
const [runAfter, setRunAfter] = useState(false);
|
|
|
|
if (!isOpen) return null;
|
|
|
|
// If creating new study, no choice - only create mode
|
|
const canUpdate = !!currentStudy && !isCreatingNew;
|
|
|
|
const handleProcess = () => {
|
|
onProcess({
|
|
mode,
|
|
studyName: mode === 'create' ? newStudyName : undefined,
|
|
category: mode === 'create' ? newCategory : undefined,
|
|
copyModelFiles: mode === 'create' && canUpdate ? copyModelFiles : false,
|
|
openAfter,
|
|
runAfter,
|
|
});
|
|
};
|
|
|
|
const displayStudyName = isCreatingNew ? pendingStudyName : currentStudy?.name;
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
|
|
<div className="bg-dark-850 border border-dark-700 rounded-xl w-full max-w-lg shadow-2xl">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between px-6 py-4 border-b border-dark-700">
|
|
<h2 className="text-lg font-semibold text-white">Generate Optimization</h2>
|
|
<button onClick={onClose} className="p-1 text-dark-400 hover:text-white">
|
|
<X size={20} />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Body */}
|
|
<div className="px-6 py-4 space-y-4">
|
|
{/* What Claude generates */}
|
|
<div className="p-3 bg-dark-800 rounded-lg">
|
|
<p className="text-sm text-dark-300 mb-2">Claude will generate:</p>
|
|
<ul className="text-sm text-dark-400 space-y-1">
|
|
<li>• optimization_config.json</li>
|
|
<li>• run_optimization.py (using Atomizer protocols)</li>
|
|
</ul>
|
|
</div>
|
|
|
|
{/* Mode Selection */}
|
|
{canUpdate ? (
|
|
<div className="space-y-3">
|
|
{/* Update existing */}
|
|
<label className={`flex items-start gap-3 p-3 border rounded-lg cursor-pointer transition-colors
|
|
${mode === 'update' ? 'bg-dark-800 border-primary-500' : 'border-dark-600 hover:border-dark-500'}`}>
|
|
<input
|
|
type="radio"
|
|
name="mode"
|
|
value="update"
|
|
checked={mode === 'update'}
|
|
onChange={() => setMode('update')}
|
|
className="mt-1"
|
|
/>
|
|
<RefreshCw size={20} className="text-dark-400 mt-0.5" />
|
|
<div>
|
|
<p className="font-medium text-white">Update current study</p>
|
|
<p className="text-sm text-dark-400">Modify: {currentStudy.name}</p>
|
|
<div className="flex items-center gap-1 mt-1 text-xs text-amber-400">
|
|
<AlertTriangle size={12} />
|
|
<span>Will overwrite existing configuration</span>
|
|
</div>
|
|
</div>
|
|
</label>
|
|
|
|
{/* Create new */}
|
|
<label className={`flex items-start gap-3 p-3 border rounded-lg cursor-pointer transition-colors
|
|
${mode === 'create' ? 'bg-dark-800 border-primary-500' : 'border-dark-600 hover:border-dark-500'}`}>
|
|
<input
|
|
type="radio"
|
|
name="mode"
|
|
value="create"
|
|
checked={mode === 'create'}
|
|
onChange={() => setMode('create')}
|
|
className="mt-1"
|
|
/>
|
|
<FolderPlus size={20} className="text-dark-400 mt-0.5" />
|
|
<div className="flex-1">
|
|
<p className="font-medium text-white">Create new study</p>
|
|
{mode === 'create' && (
|
|
<div className="mt-2 space-y-2">
|
|
<input
|
|
type="text"
|
|
value={newStudyName}
|
|
onChange={(e) => setNewStudyName(e.target.value.replace(/[^a-zA-Z0-9_-]/g, '_'))}
|
|
placeholder={`${currentStudy?.name}_v2`}
|
|
className="w-full px-2 py-1 bg-dark-700 border border-dark-600 rounded
|
|
text-sm text-white placeholder-dark-500"
|
|
/>
|
|
<select
|
|
value={newCategory}
|
|
onChange={(e) => setNewCategory(e.target.value)}
|
|
className="w-full px-2 py-1 bg-dark-700 border border-dark-600 rounded
|
|
text-sm text-white"
|
|
>
|
|
{categories.map((cat) => (
|
|
<option key={cat} value={cat}>{cat}</option>
|
|
))}
|
|
</select>
|
|
<label className="flex items-center gap-2 text-sm text-dark-300">
|
|
<input
|
|
type="checkbox"
|
|
checked={copyModelFiles}
|
|
onChange={(e) => setCopyModelFiles(e.target.checked)}
|
|
/>
|
|
Copy model files from {currentStudy.name}
|
|
</label>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</label>
|
|
</div>
|
|
) : (
|
|
/* Creating new study - no choice */
|
|
<div className="p-3 bg-dark-800 border border-dark-600 rounded-lg">
|
|
<div className="flex items-center gap-2">
|
|
<FolderPlus size={20} className="text-primary-400" />
|
|
<div>
|
|
<p className="font-medium text-white">Creating: {displayStudyName}</p>
|
|
<p className="text-sm text-dark-400">
|
|
{isCreatingNew && pendingCategory ? `Category: ${pendingCategory}` : ''}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* After creation options */}
|
|
<div className="pt-2 border-t border-dark-700 space-y-2">
|
|
<p className="text-sm font-medium text-dark-300">After generation:</p>
|
|
<label className="flex items-center gap-2 text-sm text-dark-300">
|
|
<input
|
|
type="checkbox"
|
|
checked={openAfter}
|
|
onChange={(e) => setOpenAfter(e.target.checked)}
|
|
/>
|
|
Open study automatically
|
|
</label>
|
|
<label className="flex items-center gap-2 text-sm text-dark-300">
|
|
<input
|
|
type="checkbox"
|
|
checked={runAfter}
|
|
onChange={(e) => setRunAfter(e.target.checked)}
|
|
/>
|
|
Start optimization immediately
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="flex items-center justify-end gap-3 px-6 py-4 border-t border-dark-700">
|
|
<button
|
|
onClick={onClose}
|
|
disabled={isProcessing}
|
|
className="px-4 py-2 text-dark-300 hover:text-white transition-colors disabled:opacity-50"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
onClick={handleProcess}
|
|
disabled={isProcessing || (mode === 'create' && !newStudyName && canUpdate)}
|
|
className="flex items-center gap-2 px-4 py-2 bg-primary-500 hover:bg-primary-600
|
|
disabled:bg-dark-700 disabled:text-dark-500 text-white rounded-lg
|
|
font-medium transition-colors"
|
|
>
|
|
{isProcessing ? (
|
|
<>
|
|
<Loader2 size={16} className="animate-spin" />
|
|
Processing...
|
|
</>
|
|
) : (
|
|
'Generate with Claude'
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
# PHASE 7: Backend API for Studies (NEW)
|
|
|
|
## T7.1 - Create Studies API Routes
|
|
|
|
**File:** `atomizer-dashboard/backend/api/routes/studies.py`
|
|
|
|
```python
|
|
from fastapi import APIRouter, HTTPException
|
|
from pydantic import BaseModel
|
|
from pathlib import Path
|
|
from typing import Optional, List
|
|
import json
|
|
import shutil
|
|
|
|
router = APIRouter(prefix="/api/studies", tags=["studies"])
|
|
|
|
STUDIES_ROOT = Path(__file__).parent.parent.parent.parent.parent / "studies"
|
|
|
|
|
|
class CreateStudyRequest(BaseModel):
|
|
name: str
|
|
category: str
|
|
|
|
|
|
class GenerateRequest(BaseModel):
|
|
intent: dict
|
|
overwrite: bool = False
|
|
|
|
|
|
class Study(BaseModel):
|
|
path: str
|
|
name: str
|
|
category: str
|
|
status: str
|
|
trialCount: int
|
|
maxTrials: int
|
|
bestValue: Optional[float] = None
|
|
hasConfig: bool
|
|
|
|
|
|
@router.get("")
|
|
async def list_studies():
|
|
"""List all studies with metadata."""
|
|
studies = []
|
|
categories = set()
|
|
|
|
if not STUDIES_ROOT.exists():
|
|
return {"studies": [], "categories": []}
|
|
|
|
for category_dir in STUDIES_ROOT.iterdir():
|
|
if not category_dir.is_dir() or category_dir.name.startswith('.'):
|
|
continue
|
|
|
|
categories.add(category_dir.name)
|
|
|
|
for study_dir in category_dir.iterdir():
|
|
if not study_dir.is_dir() or study_dir.name.startswith('.'):
|
|
continue
|
|
|
|
# Check for config
|
|
config_path = study_dir / "optimization_config.json"
|
|
has_config = config_path.exists()
|
|
|
|
# Get trial count from results
|
|
status = "not_started"
|
|
trial_count = 0
|
|
max_trials = 100
|
|
best_value = None
|
|
|
|
db_path = study_dir / "3_results" / "study.db"
|
|
if db_path.exists():
|
|
try:
|
|
import sqlite3
|
|
conn = sqlite3.connect(str(db_path))
|
|
cursor = conn.cursor()
|
|
|
|
# Get trial count
|
|
cursor.execute("SELECT COUNT(*) FROM trials WHERE state = 'COMPLETE'")
|
|
trial_count = cursor.fetchone()[0]
|
|
|
|
# Get best value
|
|
cursor.execute("""
|
|
SELECT MIN(value) FROM trial_values
|
|
JOIN trials ON trial_values.trial_id = trials.trial_id
|
|
WHERE trials.state = 'COMPLETE'
|
|
""")
|
|
result = cursor.fetchone()
|
|
if result and result[0] is not None:
|
|
best_value = result[0]
|
|
|
|
conn.close()
|
|
|
|
# Determine status
|
|
if trial_count > 0:
|
|
if trial_count >= max_trials:
|
|
status = "completed"
|
|
else:
|
|
# Check if running (simplified - could check PID file)
|
|
status = "paused" # Default to paused if has trials
|
|
except Exception:
|
|
pass
|
|
|
|
if has_config:
|
|
try:
|
|
with open(config_path) as f:
|
|
config = json.load(f)
|
|
max_trials = config.get("optimization", {}).get("n_trials", 100)
|
|
except Exception:
|
|
pass
|
|
|
|
studies.append(Study(
|
|
path=f"{category_dir.name}/{study_dir.name}",
|
|
name=study_dir.name,
|
|
category=category_dir.name,
|
|
status=status,
|
|
trialCount=trial_count,
|
|
maxTrials=max_trials,
|
|
bestValue=best_value,
|
|
hasConfig=has_config,
|
|
))
|
|
|
|
return {
|
|
"studies": [s.dict() for s in sorted(studies, key=lambda x: x.name)],
|
|
"categories": sorted(list(categories)),
|
|
}
|
|
|
|
|
|
@router.get("/{study_path:path}")
|
|
async def get_study(study_path: str):
|
|
"""Get single study details."""
|
|
study_dir = STUDIES_ROOT / study_path
|
|
|
|
if not study_dir.exists():
|
|
raise HTTPException(status_code=404, detail="Study not found")
|
|
|
|
parts = study_path.split("/")
|
|
if len(parts) != 2:
|
|
raise HTTPException(status_code=400, detail="Invalid study path")
|
|
|
|
category, name = parts
|
|
|
|
config_path = study_dir / "optimization_config.json"
|
|
has_config = config_path.exists()
|
|
max_trials = 100
|
|
|
|
if has_config:
|
|
try:
|
|
with open(config_path) as f:
|
|
config = json.load(f)
|
|
max_trials = config.get("optimization", {}).get("n_trials", 100)
|
|
except Exception:
|
|
pass
|
|
|
|
return Study(
|
|
path=study_path,
|
|
name=name,
|
|
category=category,
|
|
status="not_started",
|
|
trialCount=0,
|
|
maxTrials=max_trials,
|
|
bestValue=None,
|
|
hasConfig=has_config,
|
|
).dict()
|
|
|
|
|
|
@router.post("")
|
|
async def create_study(request: CreateStudyRequest):
|
|
"""Create new study folder structure."""
|
|
study_dir = STUDIES_ROOT / request.category / request.name
|
|
|
|
if study_dir.exists():
|
|
raise HTTPException(status_code=400, detail="Study already exists")
|
|
|
|
# Create folder structure
|
|
(study_dir / "1_config").mkdir(parents=True, exist_ok=True)
|
|
(study_dir / "2_iterations").mkdir(parents=True, exist_ok=True)
|
|
(study_dir / "3_results").mkdir(parents=True, exist_ok=True)
|
|
|
|
return Study(
|
|
path=f"{request.category}/{request.name}",
|
|
name=request.name,
|
|
category=request.category,
|
|
status="not_started",
|
|
trialCount=0,
|
|
maxTrials=100,
|
|
bestValue=None,
|
|
hasConfig=False,
|
|
).dict()
|
|
|
|
|
|
@router.post("/{study_path:path}/copy-models")
|
|
async def copy_model_files(study_path: str, source_study: str):
|
|
"""Copy model files from another study."""
|
|
target_dir = STUDIES_ROOT / study_path
|
|
source_dir = STUDIES_ROOT / source_study
|
|
|
|
if not target_dir.exists():
|
|
raise HTTPException(status_code=404, detail="Target study not found")
|
|
if not source_dir.exists():
|
|
raise HTTPException(status_code=404, detail="Source study not found")
|
|
|
|
copied = []
|
|
for ext in [".prt", ".sim", ".fem"]:
|
|
for f in source_dir.glob(f"*{ext}"):
|
|
target = target_dir / f.name
|
|
shutil.copy2(f, target)
|
|
copied.append(f.name)
|
|
|
|
return {"copied": copied}
|
|
|
|
|
|
@router.get("/{study_path:path}/config")
|
|
async def get_study_config(study_path: str):
|
|
"""Get optimization_config.json for a study."""
|
|
config_path = STUDIES_ROOT / study_path / "optimization_config.json"
|
|
|
|
if not config_path.exists():
|
|
raise HTTPException(status_code=404, detail="Config not found")
|
|
|
|
with open(config_path) as f:
|
|
return json.load(f)
|
|
```
|
|
|
|
## T7.2 - Register Routes in Main
|
|
|
|
**File:** `atomizer-dashboard/backend/api/main.py`
|
|
|
|
Add:
|
|
```python
|
|
from api.routes import studies
|
|
|
|
app.include_router(studies.router)
|
|
```
|
|
|
|
---
|
|
|
|
# PHASE 8: Update Canvas Header (Context-Aware)
|
|
|
|
## T8.1 - Update CanvasView Header
|
|
|
|
Update `CanvasView.tsx` to show current study context:
|
|
|
|
```tsx
|
|
// In header
|
|
<header className="flex-shrink-0 h-10 bg-dark-850 border-b border-dark-700 px-4 flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<h1 className="text-sm font-semibold text-white">Canvas Builder</h1>
|
|
{currentStudy && !isCreatingNew && (
|
|
<span className="px-2 py-0.5 bg-dark-700 rounded text-xs text-dark-300">
|
|
Editing: {currentStudy.name}
|
|
</span>
|
|
)}
|
|
{isCreatingNew && pendingStudyName && (
|
|
<span className="px-2 py-0.5 bg-primary-500/20 rounded text-xs text-primary-400">
|
|
Creating: {pendingStudyName}
|
|
</span>
|
|
)}
|
|
<span className="text-xs text-dark-500 tabular-nums">
|
|
{nodes.length} node{nodes.length !== 1 ? 's' : ''}
|
|
</span>
|
|
</div>
|
|
{/* ... rest of header */}
|
|
</header>
|
|
```
|
|
|
|
---
|
|
|
|
# PHASE 9: Build and Commit
|
|
|
|
## T9.1 - Build Frontend
|
|
|
|
```bash
|
|
cd atomizer-dashboard/frontend
|
|
npm run build
|
|
```
|
|
|
|
## T9.2 - Test Backend
|
|
|
|
```bash
|
|
cd atomizer-dashboard/backend
|
|
python -c "from api.routes.studies import router; print('Studies API OK')"
|
|
```
|
|
|
|
## T9.3 - Git Commit
|
|
|
|
```bash
|
|
git add .
|
|
git commit -m "feat: Canvas V3 - Bug fixes and study management workflow
|
|
|
|
## Bug Fixes (Phase 1-3)
|
|
- Fix Atomizer Assistant error with proper error handling + reconnect
|
|
- Enable edge selection and deletion (Delete/Backspace key)
|
|
- Fix drag & drop positioning
|
|
- Complete loadFromConfig rewrite with ALL nodes and edges
|
|
- Full-screen responsive canvas layout
|
|
- Fix contrast issues and increase font sizes
|
|
|
|
## Study Management (Phase 4-8)
|
|
- Add StudyContext for global study state management
|
|
- Create StudyBrowser component for Home page
|
|
- Add NewStudyDialog for creating studies from scratch
|
|
- Add ProcessDialog with update/create choice
|
|
- Add backend /api/studies endpoints (list, create, copy-models)
|
|
- Context-aware Canvas header showing current study
|
|
|
|
## User Flows
|
|
- Open existing study → edit in Canvas → update or create new
|
|
- Create new study from blank/template/copy → design → generate
|
|
- Home page shows all studies with quick actions
|
|
|
|
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>"
|
|
|
|
git push origin main && git push github main
|
|
```
|
|
|
|
---
|
|
|
|
# ACCEPTANCE CRITERIA
|
|
|
|
## Bug Fixes
|
|
- [ ] Chat panel opens without error
|
|
- [ ] Edges can be selected and deleted
|
|
- [ ] Drag & drop positions correctly
|
|
- [ ] Loading study creates ALL nodes and edges
|
|
- [ ] Canvas fills screen
|
|
- [ ] All text readable (good contrast)
|
|
|
|
## Study Management
|
|
- [ ] Home page lists all studies
|
|
- [ ] "New Study" button opens dialog
|
|
- [ ] Can create study from blank/template/copy
|
|
- [ ] Canvas shows "Editing: X" or "Creating: X"
|
|
- [ ] Process dialog offers update vs. create choice
|
|
- [ ] Backend /api/studies endpoints work
|
|
|
|
## Build
|
|
- [ ] `npm run build` passes
|
|
|
|
---
|
|
|
|
# BEGIN EXECUTION
|
|
|
|
Execute all 9 phases sequentially. Use TodoWrite for every task. Complete fully before moving to next phase. Do not stop.
|
|
|
|
**GO.**
|