/** * Atomizer Studio - Unified Study Creation Environment * * A drag-and-drop workspace for creating optimization studies with: * - File upload (models + context documents) * - Visual canvas configuration * - AI-powered assistance * - One-click build to final study */ import { useState, useEffect, useCallback, useRef } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { Home, ChevronRight, Upload, FileText, Settings, Sparkles, Save, RefreshCw, Trash2, MessageSquare, Layers, CheckCircle, AlertCircle, Loader2, X, ChevronLeft, ChevronRight as ChevronRightIcon, GripVertical, } from 'lucide-react'; import { intakeApi } from '../api/intake'; import { SpecRenderer } from '../components/canvas/SpecRenderer'; import { NodePalette } from '../components/canvas/palette/NodePalette'; import { NodeConfigPanelV2 } from '../components/canvas/panels/NodeConfigPanelV2'; import { useSpecStore, useSpec, useSpecLoading } from '../hooks/useSpecStore'; import { StudioDropZone } from '../components/studio/StudioDropZone'; import { StudioParameterList } from '../components/studio/StudioParameterList'; import { StudioContextFiles } from '../components/studio/StudioContextFiles'; import { StudioChat } from '../components/studio/StudioChat'; import { StudioBuildDialog } from '../components/studio/StudioBuildDialog'; interface DraftState { draftId: string | null; status: 'idle' | 'creating' | 'ready' | 'error'; error: string | null; modelFiles: string[]; contextFiles: string[]; contextContent: string; introspectionAvailable: boolean; designVariableCount: number; objectiveCount: number; } export default function Studio() { const navigate = useNavigate(); const { draftId: urlDraftId } = useParams<{ draftId: string }>(); // Draft state const [draft, setDraft] = useState({ draftId: null, status: 'idle', error: null, modelFiles: [], contextFiles: [], contextContent: '', introspectionAvailable: false, designVariableCount: 0, objectiveCount: 0, }); // UI state const [leftPanelWidth, setLeftPanelWidth] = useState(320); const [rightPanelCollapsed, setRightPanelCollapsed] = useState(false); const [showBuildDialog, setShowBuildDialog] = useState(false); const [isIntrospecting, setIsIntrospecting] = useState(false); const [notification, setNotification] = useState<{ type: 'success' | 'error' | 'info'; message: string } | null>(null); // Resize state const isResizing = useRef(false); const minPanelWidth = 280; const maxPanelWidth = 500; // Spec store for canvas const spec = useSpec(); const specLoading = useSpecLoading(); const { loadSpec, clearSpec } = useSpecStore(); // Handle panel resize const handleMouseDown = useCallback((e: React.MouseEvent) => { e.preventDefault(); isResizing.current = true; document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'; }, []); useEffect(() => { const handleMouseMove = (e: MouseEvent) => { if (!isResizing.current) return; const newWidth = Math.min(maxPanelWidth, Math.max(minPanelWidth, e.clientX)); setLeftPanelWidth(newWidth); }; const handleMouseUp = () => { isResizing.current = false; document.body.style.cursor = ''; document.body.style.userSelect = ''; }; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); return () => { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; }, []); // Initialize or load draft on mount useEffect(() => { if (urlDraftId) { loadDraft(urlDraftId); } else { createNewDraft(); } return () => { // Cleanup: clear spec when leaving Studio clearSpec(); }; }, [urlDraftId]); // Create a new draft const createNewDraft = async () => { setDraft(prev => ({ ...prev, status: 'creating', error: null })); try { const response = await intakeApi.createDraft(); setDraft({ draftId: response.draft_id, status: 'ready', error: null, modelFiles: [], contextFiles: [], contextContent: '', introspectionAvailable: false, designVariableCount: 0, objectiveCount: 0, }); // Update URL without navigation window.history.replaceState(null, '', `/studio/${response.draft_id}`); // Load the empty spec for this draft await loadSpec(response.draft_id); showNotification('info', 'New studio session started. Drop your files to begin.'); } catch (err) { setDraft(prev => ({ ...prev, status: 'error', error: err instanceof Error ? err.message : 'Failed to create draft', })); } }; // Load existing draft or study const loadDraft = async (studyId: string) => { setDraft(prev => ({ ...prev, status: 'creating', error: null })); // Check if this is a draft (in _inbox) or an existing study const isDraft = studyId.startsWith('draft_'); if (isDraft) { // Load from intake API try { const response = await intakeApi.getStudioDraft(studyId); // Also load context content if there are context files let contextContent = ''; if (response.context_files.length > 0) { try { const contextResponse = await intakeApi.getContextContent(studyId); contextContent = contextResponse.content; } catch { // Ignore context loading errors } } setDraft({ draftId: response.draft_id, status: 'ready', error: null, modelFiles: response.model_files, contextFiles: response.context_files, contextContent, introspectionAvailable: response.introspection_available, designVariableCount: response.design_variable_count, objectiveCount: response.objective_count, }); // Load the spec await loadSpec(studyId); showNotification('info', `Resuming draft: ${studyId}`); } catch (err) { // Draft doesn't exist, create new one createNewDraft(); } } else { // Load existing study directly via spec store try { await loadSpec(studyId); // Get counts from loaded spec const loadedSpec = useSpecStore.getState().spec; setDraft({ draftId: studyId, status: 'ready', error: null, modelFiles: [], // Existing studies don't track files separately contextFiles: [], contextContent: '', introspectionAvailable: true, // Assume introspection was done designVariableCount: loadedSpec?.design_variables?.length || 0, objectiveCount: loadedSpec?.objectives?.length || 0, }); showNotification('info', `Editing study: ${studyId}`); } catch (err) { setDraft(prev => ({ ...prev, status: 'error', error: err instanceof Error ? err.message : 'Failed to load study', })); } } }; // Refresh draft data const refreshDraft = async () => { if (!draft.draftId) return; const isDraft = draft.draftId.startsWith('draft_'); if (isDraft) { try { const response = await intakeApi.getStudioDraft(draft.draftId); // Also refresh context content let contextContent = draft.contextContent; if (response.context_files.length > 0) { try { const contextResponse = await intakeApi.getContextContent(draft.draftId); contextContent = contextResponse.content; } catch { // Keep existing content } } setDraft(prev => ({ ...prev, modelFiles: response.model_files, contextFiles: response.context_files, contextContent, introspectionAvailable: response.introspection_available, designVariableCount: response.design_variable_count, objectiveCount: response.objective_count, })); // Reload spec await loadSpec(draft.draftId); } catch (err) { showNotification('error', 'Failed to refresh draft'); } } else { // For existing studies, just reload the spec try { await loadSpec(draft.draftId); const loadedSpec = useSpecStore.getState().spec; setDraft(prev => ({ ...prev, designVariableCount: loadedSpec?.design_variables?.length || 0, objectiveCount: loadedSpec?.objectives?.length || 0, })); } catch (err) { showNotification('error', 'Failed to refresh study'); } } }; // Run introspection const runIntrospection = async () => { if (!draft.draftId || draft.modelFiles.length === 0) { showNotification('error', 'Please upload model files first'); return; } setIsIntrospecting(true); try { const response = await intakeApi.introspect({ study_name: draft.draftId }); showNotification('success', `Found ${response.expressions_count} expressions (${response.candidates_count} candidates)`); // Refresh draft state await refreshDraft(); } catch (err) { showNotification('error', err instanceof Error ? err.message : 'Introspection failed'); } finally { setIsIntrospecting(false); } }; // Handle file upload complete const handleUploadComplete = useCallback(() => { refreshDraft(); showNotification('success', 'Files uploaded successfully'); }, [draft.draftId]); // Handle build complete const handleBuildComplete = (finalPath: string, finalName: string) => { setShowBuildDialog(false); showNotification('success', `Study "${finalName}" created successfully!`); // Navigate to the new study setTimeout(() => { navigate(`/canvas/${finalPath.replace('studies/', '')}`); }, 1500); }; // Reset draft const resetDraft = async () => { if (!draft.draftId) return; if (!confirm('Are you sure you want to reset? This will delete all uploaded files and configurations.')) { return; } try { await intakeApi.deleteInboxStudy(draft.draftId); await createNewDraft(); } catch (err) { showNotification('error', 'Failed to reset draft'); } }; // Show notification const showNotification = (type: 'success' | 'error' | 'info', message: string) => { setNotification({ type, message }); setTimeout(() => setNotification(null), 4000); }; // Can always save/build - even empty studies can be saved for later const canBuild = draft.draftId !== null; // Loading state if (draft.status === 'creating') { return (

Initializing Studio...

); } // Error state if (draft.status === 'error') { return (

Failed to Initialize

{draft.error}

); } return (
{/* Header */}
{/* Left: Navigation */}
Atomizer Studio
{draft.draftId && ( <> {draft.draftId} )}
{/* Right: Actions */}
{/* Main Content */}
{/* Left Panel: Resources (Resizable) */}
{/* Drop Zone */}

Model Files

{draft.draftId && ( )}
{/* Introspection */} {draft.modelFiles.length > 0 && (

Parameters

{draft.draftId && draft.introspectionAvailable && ( )} {!draft.introspectionAvailable && (

Click "Scan" to discover parameters from your model.

)}
)} {/* Context Files */}

Context Documents

{draft.draftId && ( )}

Upload requirements, goals, or specs. The AI will read these.

{/* Show context preview if loaded */} {draft.contextContent && (

Context Loaded:

{draft.contextContent.substring(0, 200)}...

)}
{/* Node Palette - EXPANDED, not collapsed */}

Components

{/* Resize Handle */}
{/* Center: Canvas */}
{draft.draftId && ( )} {/* Empty state */} {!specLoading && (!spec || Object.keys(spec).length === 0) && (

Welcome to Atomizer Studio

Drop your model files on the left, or drag components from the palette to start building your optimization study.

Upload .sim, .prt, .fem files
Add context documents (PDF, MD, TXT)
Configure with AI assistance
)}
{/* Right Panel: Assistant + Config - wider for better chat UX */}
{/* Collapse toggle */} {!rightPanelCollapsed && (
{/* Chat */}
{draft.draftId && ( )}
{/* Config Panel (when node selected) */}
)} {rightPanelCollapsed && (
)}
{/* Notification Toast */} {notification && (
{notification.type === 'success' && } {notification.type === 'error' && } {notification.type === 'info' && } {notification.message}
)} {/* Build Dialog */} {showBuildDialog && draft.draftId && ( setShowBuildDialog(false)} onBuildComplete={handleBuildComplete} /> )}
); }