feat: Add Claude Code terminal integration to dashboard

- Add embedded Claude Code terminal with xterm.js for full CLI experience
- Create WebSocket PTY backend for real-time terminal communication
- Add terminal status endpoint to check CLI availability
- Update dashboard to use Claude Code terminal instead of API chat
- Add optimization control panel with start/stop/validate actions
- Add study context provider for global state management
- Update frontend with new dependencies (xterm.js addons)
- Comprehensive README documentation for all new features

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Antoine
2025-12-04 15:02:13 -05:00
parent 8cbdbcad78
commit 9eed4d81eb
23 changed files with 5060 additions and 339 deletions

View File

@@ -1,25 +1,38 @@
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { StudyProvider } from './context/StudyContext';
import { MainLayout } from './components/layout/MainLayout';
import Home from './pages/Home';
import Dashboard from './pages/Dashboard';
import Configurator from './pages/Configurator';
import Results from './pages/Results';
const queryClient = new QueryClient();
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5000,
refetchOnWindowFocus: false,
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<Routes>
<Route path="/" element={<MainLayout />}>
<Route index element={<Navigate to="/dashboard" replace />} />
<Route path="dashboard" element={<Dashboard />} />
<Route path="configurator" element={<Configurator />} />
<Route path="results" element={<Results />} />
</Route>
</Routes>
</BrowserRouter>
<StudyProvider>
<BrowserRouter>
<Routes>
{/* Home page - no sidebar layout */}
<Route path="/" element={<Home />} />
{/* Study pages - with sidebar layout */}
<Route element={<MainLayout />}>
<Route path="dashboard" element={<Dashboard />} />
<Route path="results" element={<Results />} />
<Route path="analytics" element={<Dashboard />} />
</Route>
</Routes>
</BrowserRouter>
</StudyProvider>
</QueryClientProvider>
);
}

View File

@@ -2,6 +2,56 @@ import { StudyListResponse, HistoryResponse, PruningResponse, StudyStatus } from
const API_BASE = '/api';
export interface OptimizationControlResponse {
success: boolean;
message: string;
pid?: number;
}
export interface ReadmeResponse {
content: string;
path: string;
}
export interface ReportResponse {
content: string;
generated_at?: string;
}
export interface ConfigResponse {
config: Record<string, any>;
objectives: Array<{
name: string;
direction: string;
weight?: number;
target?: number;
units?: string;
}>;
design_variables: Array<{
name: string;
min: number;
max: number;
baseline?: number;
units?: string;
}>;
constraints?: Array<{
name: string;
type: string;
max_value?: number;
min_value?: number;
units?: string;
}>;
}
export interface ProcessStatus {
is_running: boolean;
pid?: number;
start_time?: string;
iteration?: number;
fea_count?: number;
nn_count?: number;
}
class ApiClient {
async getStudies(): Promise<StudyListResponse> {
const response = await fetch(`${API_BASE}/optimization/studies`);
@@ -37,12 +87,24 @@ class ApiClient {
return response.json();
}
async getStudyReport(studyId: string): Promise<{ content: string }> {
async getStudyReport(studyId: string): Promise<ReportResponse> {
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/report`);
if (!response.ok) throw new Error('Failed to fetch report');
return response.json();
}
async getStudyReadme(studyId: string): Promise<ReadmeResponse> {
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/readme`);
if (!response.ok) throw new Error('Failed to fetch README');
return response.json();
}
async getStudyConfig(studyId: string): Promise<ConfigResponse> {
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/config`);
if (!response.ok) throw new Error('Failed to fetch config');
return response.json();
}
async getConsoleOutput(studyId: string, lines: number = 200): Promise<{
lines: string[];
total_lines: number;
@@ -56,16 +118,81 @@ class ApiClient {
return response.json();
}
// Future endpoints for control
async startOptimization(studyId: string): Promise<void> {
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/start`, { method: 'POST' });
if (!response.ok) throw new Error('Failed to start optimization');
async getProcessStatus(studyId: string): Promise<ProcessStatus> {
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/process`);
if (!response.ok) throw new Error('Failed to fetch process status');
return response.json();
}
async stopOptimization(studyId: string): Promise<void> {
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/stop`, { method: 'POST' });
if (!response.ok) throw new Error('Failed to stop optimization');
// Control operations
async startOptimization(studyId: string, options?: {
freshStart?: boolean;
maxIterations?: number;
feaBatchSize?: number;
tuneTrials?: number;
ensembleSize?: number;
patience?: number;
}): Promise<OptimizationControlResponse> {
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(options || {}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to start optimization');
}
return response.json();
}
async stopOptimization(studyId: string): Promise<OptimizationControlResponse> {
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/stop`, {
method: 'POST',
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to stop optimization');
}
return response.json();
}
async validateOptimization(studyId: string, options?: {
topN?: number;
}): Promise<OptimizationControlResponse> {
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/validate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(options || {}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to start validation');
}
return response.json();
}
async generateReport(studyId: string): Promise<ReportResponse> {
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/report/generate`, {
method: 'POST',
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to generate report');
}
return response.json();
}
// Optuna dashboard
async launchOptunaDashboard(studyId: string): Promise<{ url: string; pid: number }> {
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/optuna-dashboard`, {
method: 'POST',
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to launch Optuna dashboard');
}
return response.json();
}
}
export const apiClient = new ApiClient();
export const apiClient = new ApiClient();

View File

@@ -0,0 +1,450 @@
import React, { useState, useRef, useEffect } from 'react';
import {
Send,
Bot,
User,
Sparkles,
Loader2,
X,
Maximize2,
Minimize2,
AlertCircle,
Wrench,
ChevronDown,
ChevronUp,
Trash2
} from 'lucide-react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { useStudy } from '../context/StudyContext';
interface Message {
id: string;
role: 'user' | 'assistant';
content: string;
timestamp: Date;
toolCalls?: Array<{
tool: string;
input: Record<string, any>;
result_preview: string;
}>;
}
interface ClaudeChatProps {
isExpanded?: boolean;
onToggleExpand?: () => void;
onClose?: () => void;
}
export const ClaudeChat: React.FC<ClaudeChatProps> = ({
isExpanded = false,
onToggleExpand,
onClose
}) => {
const { selectedStudy } = useStudy();
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [apiAvailable, setApiAvailable] = useState<boolean | null>(null);
const [suggestions, setSuggestions] = useState<string[]>([]);
const [expandedTools, setExpandedTools] = useState<Set<string>>(new Set());
const messagesEndRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
// Check API status on mount
useEffect(() => {
checkApiStatus();
}, []);
// Load suggestions when study changes
useEffect(() => {
loadSuggestions();
}, [selectedStudy]);
// Scroll to bottom when messages change
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
const checkApiStatus = async () => {
try {
const response = await fetch('/api/claude/status');
const data = await response.json();
setApiAvailable(data.available);
if (!data.available) {
setError(data.message);
}
} catch (err) {
setApiAvailable(false);
setError('Could not connect to Claude API');
}
};
const loadSuggestions = async () => {
try {
const url = selectedStudy
? `/api/claude/suggestions?study_id=${selectedStudy.id}`
: '/api/claude/suggestions';
const response = await fetch(url);
const data = await response.json();
setSuggestions(data.suggestions || []);
} catch (err) {
setSuggestions([]);
}
};
const sendMessage = async (messageText?: string) => {
const text = messageText || input.trim();
if (!text || isLoading) return;
setError(null);
const userMessage: Message = {
id: `user-${Date.now()}`,
role: 'user',
content: text,
timestamp: new Date()
};
setMessages(prev => [...prev, userMessage]);
setInput('');
setIsLoading(true);
try {
const response = await fetch('/api/claude/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: text,
study_id: selectedStudy?.id,
conversation_history: messages.map(m => ({
role: m.role,
content: m.content
}))
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || 'Failed to get response');
}
const data = await response.json();
const assistantMessage: Message = {
id: `assistant-${Date.now()}`,
role: 'assistant',
content: data.response,
timestamp: new Date(),
toolCalls: data.tool_calls
};
setMessages(prev => [...prev, assistantMessage]);
} catch (err: any) {
setError(err.message || 'Failed to send message');
} finally {
setIsLoading(false);
inputRef.current?.focus();
}
};
const clearConversation = () => {
setMessages([]);
setError(null);
};
const toggleToolExpand = (toolId: string) => {
setExpandedTools(prev => {
const newSet = new Set(prev);
if (newSet.has(toolId)) {
newSet.delete(toolId);
} else {
newSet.add(toolId);
}
return newSet;
});
};
// Render tool call indicator
const renderToolCalls = (toolCalls: Message['toolCalls'], messageId: string) => {
if (!toolCalls || toolCalls.length === 0) return null;
return (
<div className="mt-2 space-y-1">
{toolCalls.map((tool, index) => {
const toolId = `${messageId}-tool-${index}`;
const isExpanded = expandedTools.has(toolId);
return (
<div
key={index}
className="bg-dark-700/50 rounded-lg border border-dark-600 overflow-hidden"
>
<button
onClick={() => toggleToolExpand(toolId)}
className="w-full px-3 py-2 flex items-center justify-between text-xs hover:bg-dark-600/50 transition-colors"
>
<div className="flex items-center gap-2">
<Wrench className="w-3 h-3 text-primary-400" />
<span className="text-dark-300">Used tool: </span>
<span className="text-primary-400 font-mono">{tool.tool}</span>
</div>
{isExpanded ? (
<ChevronUp className="w-3 h-3 text-dark-400" />
) : (
<ChevronDown className="w-3 h-3 text-dark-400" />
)}
</button>
{isExpanded && (
<div className="px-3 py-2 border-t border-dark-600 text-xs">
<div className="text-dark-400 mb-1">Input:</div>
<pre className="text-dark-300 bg-dark-800 p-2 rounded overflow-x-auto">
{JSON.stringify(tool.input, null, 2)}
</pre>
<div className="text-dark-400 mt-2 mb-1">Result preview:</div>
<pre className="text-dark-300 bg-dark-800 p-2 rounded overflow-x-auto whitespace-pre-wrap">
{tool.result_preview}
</pre>
</div>
)}
</div>
);
})}
</div>
);
};
return (
<div className={`flex flex-col bg-dark-800 rounded-xl border border-dark-600 overflow-hidden ${
isExpanded ? 'fixed inset-4 z-50' : 'h-full'
}`}>
{/* Header */}
<div className="px-4 py-3 border-b border-dark-600 flex items-center justify-between bg-dark-800">
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-gradient-to-br from-primary-500 to-primary-700 rounded-lg flex items-center justify-center">
<Bot className="w-4 h-4 text-white" />
</div>
<div>
<span className="font-medium text-white">Claude Code</span>
{selectedStudy && (
<span className="ml-2 text-xs bg-dark-700 px-2 py-0.5 rounded text-dark-300">
{selectedStudy.id}
</span>
)}
</div>
</div>
<div className="flex items-center gap-1">
{messages.length > 0 && (
<button
onClick={clearConversation}
className="p-2 text-dark-400 hover:text-white hover:bg-dark-700 rounded-lg transition-colors"
title="Clear conversation"
>
<Trash2 className="w-4 h-4" />
</button>
)}
{onToggleExpand && (
<button
onClick={onToggleExpand}
className="p-2 text-dark-400 hover:text-white hover:bg-dark-700 rounded-lg transition-colors"
>
{isExpanded ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
</button>
)}
{onClose && (
<button
onClick={onClose}
className="p-2 text-dark-400 hover:text-white hover:bg-dark-700 rounded-lg transition-colors"
>
<X className="w-4 h-4" />
</button>
)}
</div>
</div>
{/* API Status Warning */}
{apiAvailable === false && (
<div className="px-4 py-3 bg-yellow-900/20 border-b border-yellow-800/30 flex items-center gap-2">
<AlertCircle className="w-4 h-4 text-yellow-500" />
<span className="text-yellow-400 text-sm">
{error || 'Claude API not available'}
</span>
</div>
)}
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.length === 0 ? (
<div className="text-center py-8">
<Sparkles className="w-12 h-12 mx-auto mb-4 text-primary-400 opacity-50" />
<p className="text-dark-300 mb-2">Ask me anything about your optimization</p>
<p className="text-dark-500 text-sm mb-6">
I can analyze results, explain concepts, and help you improve your designs.
</p>
{/* Suggestions */}
{suggestions.length > 0 && (
<div className="flex flex-wrap gap-2 justify-center max-w-md mx-auto">
{suggestions.slice(0, 6).map((suggestion, i) => (
<button
key={i}
onClick={() => sendMessage(suggestion)}
disabled={isLoading || apiAvailable === false}
className="px-3 py-1.5 bg-dark-700 hover:bg-dark-600 disabled:opacity-50
rounded-lg text-sm text-dark-300 hover:text-white transition-colors
border border-dark-600 hover:border-dark-500"
>
{suggestion}
</button>
))}
</div>
)}
</div>
) : (
messages.map((msg) => (
<div
key={msg.id}
className={`flex gap-3 ${msg.role === 'user' ? 'justify-end' : ''}`}
>
{msg.role === 'assistant' && (
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center flex-shrink-0">
<Bot className="w-4 h-4 text-white" />
</div>
)}
<div
className={`max-w-[85%] rounded-lg ${
msg.role === 'user'
? 'bg-primary-600 text-white px-4 py-2'
: 'bg-dark-700 text-dark-200 px-4 py-3'
}`}
>
{msg.role === 'assistant' ? (
<>
<div className="prose prose-sm prose-invert max-w-none">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
// Simplified markdown styling for chat
p: ({ children }) => <p className="mb-2 last:mb-0">{children}</p>,
ul: ({ children }) => <ul className="list-disc list-inside mb-2">{children}</ul>,
ol: ({ children }) => <ol className="list-decimal list-inside mb-2">{children}</ol>,
li: ({ children }) => <li className="mb-1">{children}</li>,
code: ({ inline, children }: any) =>
inline ? (
<code className="px-1 py-0.5 bg-dark-600 rounded text-primary-400 text-xs">
{children}
</code>
) : (
<pre className="p-2 bg-dark-800 rounded overflow-x-auto text-xs my-2">
<code>{children}</code>
</pre>
),
table: ({ children }) => (
<div className="overflow-x-auto my-2">
<table className="text-xs border-collapse">{children}</table>
</div>
),
th: ({ children }) => (
<th className="border border-dark-600 px-2 py-1 bg-dark-800 text-left">
{children}
</th>
),
td: ({ children }) => (
<td className="border border-dark-600 px-2 py-1">{children}</td>
),
strong: ({ children }) => <strong className="text-white">{children}</strong>,
h1: ({ children }) => <h1 className="text-lg font-bold text-white mt-4 mb-2">{children}</h1>,
h2: ({ children }) => <h2 className="text-base font-semibold text-white mt-3 mb-2">{children}</h2>,
h3: ({ children }) => <h3 className="text-sm font-semibold text-white mt-2 mb-1">{children}</h3>,
}}
>
{msg.content}
</ReactMarkdown>
</div>
{renderToolCalls(msg.toolCalls, msg.id)}
</>
) : (
<p>{msg.content}</p>
)}
</div>
{msg.role === 'user' && (
<div className="w-8 h-8 rounded-lg bg-dark-600 flex items-center justify-center flex-shrink-0">
<User className="w-4 h-4 text-dark-300" />
</div>
)}
</div>
))
)}
{/* Loading indicator */}
{isLoading && (
<div className="flex gap-3">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center">
<Loader2 className="w-4 h-4 text-white animate-spin" />
</div>
<div className="bg-dark-700 rounded-lg px-4 py-3 text-dark-400">
<span className="inline-flex items-center gap-2">
<span>Thinking</span>
<span className="flex gap-1">
<span className="w-1.5 h-1.5 bg-dark-400 rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
<span className="w-1.5 h-1.5 bg-dark-400 rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
<span className="w-1.5 h-1.5 bg-dark-400 rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
</span>
</span>
</div>
</div>
)}
{/* Error message */}
{error && !isLoading && (
<div className="flex items-center gap-2 px-4 py-2 bg-red-900/20 border border-red-800/30 rounded-lg">
<AlertCircle className="w-4 h-4 text-red-400" />
<span className="text-red-400 text-sm">{error}</span>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Input */}
<div className="p-4 border-t border-dark-600 bg-dark-800">
<div className="flex gap-2">
<input
ref={inputRef}
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
}}
placeholder={apiAvailable === false ? 'API not available...' : 'Ask about your optimization...'}
disabled={isLoading || apiAvailable === false}
className="flex-1 px-4 py-2.5 bg-dark-700 border border-dark-600 rounded-lg
text-white placeholder-dark-400 focus:outline-none focus:border-primary-500
disabled:opacity-50 disabled:cursor-not-allowed"
/>
<button
onClick={() => sendMessage()}
disabled={!input.trim() || isLoading || apiAvailable === false}
className="px-4 py-2.5 bg-primary-600 hover:bg-primary-500 disabled:opacity-50
disabled:cursor-not-allowed text-white rounded-lg transition-colors"
>
{isLoading ? (
<Loader2 className="w-5 h-5 animate-spin" />
) : (
<Send className="w-5 h-5" />
)}
</button>
</div>
<p className="text-xs text-dark-500 mt-2 text-center">
Claude can query your study data, analyze results, and help improve your optimization.
</p>
</div>
</div>
);
};
export default ClaudeChat;

View File

@@ -0,0 +1,336 @@
import React, { useEffect, useRef, useState, useCallback } from 'react';
import { Terminal } from 'xterm';
import { FitAddon } from '@xterm/addon-fit';
import { WebLinksAddon } from '@xterm/addon-web-links';
import 'xterm/css/xterm.css';
import {
Terminal as TerminalIcon,
Maximize2,
Minimize2,
X,
RefreshCw,
AlertCircle
} from 'lucide-react';
import { useStudy } from '../context/StudyContext';
interface ClaudeTerminalProps {
isExpanded?: boolean;
onToggleExpand?: () => void;
onClose?: () => void;
}
export const ClaudeTerminal: React.FC<ClaudeTerminalProps> = ({
isExpanded = false,
onToggleExpand,
onClose
}) => {
const { selectedStudy } = useStudy();
const terminalRef = useRef<HTMLDivElement>(null);
const xtermRef = useRef<Terminal | null>(null);
const fitAddonRef = useRef<FitAddon | null>(null);
const wsRef = useRef<WebSocket | null>(null);
const [isConnected, setIsConnected] = useState(false);
const [isConnecting, setIsConnecting] = useState(false);
const [_error, setError] = useState<string | null>(null);
const [cliAvailable, setCliAvailable] = useState<boolean | null>(null);
// Check CLI availability
useEffect(() => {
fetch('/api/terminal/status')
.then(res => res.json())
.then(data => {
setCliAvailable(data.available);
if (!data.available) {
setError(data.message);
}
})
.catch(() => {
setCliAvailable(false);
setError('Could not check Claude Code CLI status');
});
}, []);
// Initialize terminal
useEffect(() => {
if (!terminalRef.current || xtermRef.current) return;
const term = new Terminal({
cursorBlink: true,
fontSize: 13,
fontFamily: '"JetBrains Mono", "Fira Code", Consolas, monospace',
theme: {
background: '#0f172a',
foreground: '#e2e8f0',
cursor: '#60a5fa',
cursorAccent: '#0f172a',
selectionBackground: '#334155',
black: '#1e293b',
red: '#ef4444',
green: '#22c55e',
yellow: '#eab308',
blue: '#3b82f6',
magenta: '#a855f7',
cyan: '#06b6d4',
white: '#f1f5f9',
brightBlack: '#475569',
brightRed: '#f87171',
brightGreen: '#4ade80',
brightYellow: '#facc15',
brightBlue: '#60a5fa',
brightMagenta: '#c084fc',
brightCyan: '#22d3ee',
brightWhite: '#f8fafc',
},
allowProposedApi: true,
});
const fitAddon = new FitAddon();
const webLinksAddon = new WebLinksAddon();
term.loadAddon(fitAddon);
term.loadAddon(webLinksAddon);
term.open(terminalRef.current);
// Initial fit
setTimeout(() => fitAddon.fit(), 0);
xtermRef.current = term;
fitAddonRef.current = fitAddon;
// Welcome message
term.writeln('\x1b[1;36m╔══════════════════════════════════════════════════════════╗\x1b[0m');
term.writeln('\x1b[1;36m║\x1b[0m \x1b[1;37mClaude Code Terminal\x1b[0m \x1b[1;36m║\x1b[0m');
term.writeln('\x1b[1;36m║\x1b[0m \x1b[90mFull Claude Code experience in the Atomizer dashboard\x1b[0m \x1b[1;36m║\x1b[0m');
term.writeln('\x1b[1;36m╚══════════════════════════════════════════════════════════╝\x1b[0m');
term.writeln('');
if (cliAvailable === false) {
term.writeln('\x1b[1;31mError:\x1b[0m Claude Code CLI not found.');
term.writeln('Install with: \x1b[1;33mnpm install -g @anthropic-ai/claude-code\x1b[0m');
} else {
term.writeln('\x1b[90mClick "Connect" to start a Claude Code session.\x1b[0m');
term.writeln('\x1b[90mClaude will have access to CLAUDE.md and .claude/ skills.\x1b[0m');
}
term.writeln('');
return () => {
term.dispose();
xtermRef.current = null;
};
}, [cliAvailable]);
// Handle resize
useEffect(() => {
const handleResize = () => {
if (fitAddonRef.current) {
fitAddonRef.current.fit();
// Send resize to backend
if (wsRef.current?.readyState === WebSocket.OPEN && xtermRef.current) {
wsRef.current.send(JSON.stringify({
type: 'resize',
cols: xtermRef.current.cols,
rows: xtermRef.current.rows
}));
}
}
};
window.addEventListener('resize', handleResize);
// Also fit when expanded state changes
setTimeout(handleResize, 100);
return () => window.removeEventListener('resize', handleResize);
}, [isExpanded]);
// Connect to terminal WebSocket
const connect = useCallback(() => {
if (!xtermRef.current || wsRef.current?.readyState === WebSocket.OPEN) return;
setIsConnecting(true);
setError(null);
// Determine working directory - use study path if available
let workingDir = '';
if (selectedStudy?.id) {
// The study directory path
workingDir = `?working_dir=C:/Users/Antoine/Atomizer`;
}
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const ws = new WebSocket(`${protocol}//${window.location.host}/api/terminal/claude${workingDir}`);
ws.onopen = () => {
setIsConnected(true);
setIsConnecting(false);
xtermRef.current?.clear();
xtermRef.current?.writeln('\x1b[1;32mConnected to Claude Code\x1b[0m');
xtermRef.current?.writeln('');
// Send initial resize
if (xtermRef.current) {
ws.send(JSON.stringify({
type: 'resize',
cols: xtermRef.current.cols,
rows: xtermRef.current.rows
}));
}
};
ws.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
switch (message.type) {
case 'output':
xtermRef.current?.write(message.data);
break;
case 'started':
xtermRef.current?.writeln(`\x1b[90m${message.message}\x1b[0m`);
break;
case 'exit':
xtermRef.current?.writeln('');
xtermRef.current?.writeln(`\x1b[33mClaude Code exited with code ${message.code}\x1b[0m`);
setIsConnected(false);
break;
case 'error':
xtermRef.current?.writeln(`\x1b[1;31mError: ${message.message}\x1b[0m`);
setError(message.message);
break;
}
} catch (e) {
// Raw output
xtermRef.current?.write(event.data);
}
};
ws.onerror = () => {
setError('WebSocket connection error');
setIsConnecting(false);
};
ws.onclose = () => {
setIsConnected(false);
setIsConnecting(false);
xtermRef.current?.writeln('');
xtermRef.current?.writeln('\x1b[90mDisconnected from Claude Code\x1b[0m');
};
// Handle terminal input
const disposable = xtermRef.current.onData((data) => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'input', data }));
}
});
wsRef.current = ws;
return () => {
disposable.dispose();
};
}, [selectedStudy]);
// Disconnect
const disconnect = useCallback(() => {
if (wsRef.current) {
wsRef.current.send(JSON.stringify({ type: 'stop' }));
wsRef.current.close();
wsRef.current = null;
}
setIsConnected(false);
}, []);
// Cleanup on unmount
useEffect(() => {
return () => {
if (wsRef.current) {
wsRef.current.close();
}
};
}, []);
return (
<div className={`flex flex-col bg-dark-800 rounded-xl border border-dark-600 overflow-hidden ${
isExpanded ? 'fixed inset-4 z-50' : 'h-full'
}`}>
{/* Header */}
<div className="px-4 py-3 border-b border-dark-600 flex items-center justify-between bg-dark-800">
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-gradient-to-br from-primary-500 to-primary-700 rounded-lg flex items-center justify-center">
<TerminalIcon className="w-4 h-4 text-white" />
</div>
<div>
<span className="font-medium text-white">Claude Code</span>
{selectedStudy && (
<span className="ml-2 text-xs bg-dark-700 px-2 py-0.5 rounded text-dark-300">
{selectedStudy.id}
</span>
)}
</div>
{/* Connection status indicator */}
<div className={`w-2 h-2 rounded-full ml-2 ${
isConnected ? 'bg-green-500' : 'bg-dark-500'
}`} />
</div>
<div className="flex items-center gap-1">
{/* Connect/Disconnect button */}
<button
onClick={isConnected ? disconnect : connect}
disabled={isConnecting || cliAvailable === false}
className={`px-3 py-1.5 text-sm rounded-lg transition-colors flex items-center gap-2 ${
isConnected
? 'bg-red-600/20 text-red-400 hover:bg-red-600/30'
: 'bg-green-600/20 text-green-400 hover:bg-green-600/30'
} disabled:opacity-50 disabled:cursor-not-allowed`}
>
{isConnecting ? (
<RefreshCw className="w-3 h-3 animate-spin" />
) : null}
{isConnected ? 'Disconnect' : 'Connect'}
</button>
{onToggleExpand && (
<button
onClick={onToggleExpand}
className="p-2 text-dark-400 hover:text-white hover:bg-dark-700 rounded-lg transition-colors"
>
{isExpanded ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
</button>
)}
{onClose && (
<button
onClick={onClose}
className="p-2 text-dark-400 hover:text-white hover:bg-dark-700 rounded-lg transition-colors"
>
<X className="w-4 h-4" />
</button>
)}
</div>
</div>
{/* CLI not available warning */}
{cliAvailable === false && (
<div className="px-4 py-3 bg-yellow-900/20 border-b border-yellow-800/30 flex items-center gap-2">
<AlertCircle className="w-4 h-4 text-yellow-500" />
<span className="text-yellow-400 text-sm">
Claude Code CLI not found. Install with: npm install -g @anthropic-ai/claude-code
</span>
</div>
)}
{/* Terminal */}
<div className="flex-1 p-2 bg-[#0f172a]">
<div ref={terminalRef} className="h-full" />
</div>
{/* Footer */}
<div className="px-4 py-2 border-t border-dark-600 bg-dark-800">
<p className="text-xs text-dark-500 text-center">
Claude Code has access to CLAUDE.md instructions and .claude/ skills for Atomizer optimization
</p>
</div>
</div>
);
};
export default ClaudeTerminal;

View File

@@ -0,0 +1,355 @@
import React, { useState, useEffect } from 'react';
import {
Play,
CheckCircle,
Settings,
AlertTriangle,
Loader2,
ExternalLink,
Sliders,
Skull
} from 'lucide-react';
import { apiClient, ProcessStatus } from '../../api/client';
import { useStudy } from '../../context/StudyContext';
interface ControlPanelProps {
onStatusChange?: () => void;
}
export const ControlPanel: React.FC<ControlPanelProps> = ({ onStatusChange }) => {
const { selectedStudy, refreshStudies } = useStudy();
const [processStatus, setProcessStatus] = useState<ProcessStatus | null>(null);
const [actionInProgress, setActionInProgress] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [showSettings, setShowSettings] = useState(false);
// Settings for starting optimization
const [settings, setSettings] = useState({
freshStart: false,
maxIterations: 100,
feaBatchSize: 5,
tuneTrials: 30,
ensembleSize: 3,
patience: 5,
});
// Validate top N
const [validateTopN, setValidateTopN] = useState(5);
useEffect(() => {
if (selectedStudy) {
fetchProcessStatus();
const interval = setInterval(fetchProcessStatus, 5000);
return () => clearInterval(interval);
}
}, [selectedStudy]);
const fetchProcessStatus = async () => {
if (!selectedStudy) return;
try {
const status = await apiClient.getProcessStatus(selectedStudy.id);
setProcessStatus(status);
} catch (err) {
// Process status endpoint might not exist yet
setProcessStatus(null);
}
};
const handleStart = async () => {
if (!selectedStudy) return;
setActionInProgress('start');
setError(null);
try {
await apiClient.startOptimization(selectedStudy.id, {
freshStart: settings.freshStart,
maxIterations: settings.maxIterations,
feaBatchSize: settings.feaBatchSize,
tuneTrials: settings.tuneTrials,
ensembleSize: settings.ensembleSize,
patience: settings.patience,
});
await fetchProcessStatus();
await refreshStudies();
onStatusChange?.();
} catch (err: any) {
setError(err.message || 'Failed to start optimization');
} finally {
setActionInProgress(null);
}
};
const handleStop = async () => {
if (!selectedStudy) return;
setActionInProgress('stop');
setError(null);
try {
await apiClient.stopOptimization(selectedStudy.id);
await fetchProcessStatus();
await refreshStudies();
onStatusChange?.();
} catch (err: any) {
setError(err.message || 'Failed to stop optimization');
} finally {
setActionInProgress(null);
}
};
const handleValidate = async () => {
if (!selectedStudy) return;
setActionInProgress('validate');
setError(null);
try {
await apiClient.validateOptimization(selectedStudy.id, { topN: validateTopN });
await fetchProcessStatus();
await refreshStudies();
onStatusChange?.();
} catch (err: any) {
setError(err.message || 'Failed to start validation');
} finally {
setActionInProgress(null);
}
};
const handleLaunchOptuna = async () => {
if (!selectedStudy) return;
setActionInProgress('optuna');
setError(null);
try {
const result = await apiClient.launchOptunaDashboard(selectedStudy.id);
window.open(result.url, '_blank');
} catch (err: any) {
setError(err.message || 'Failed to launch Optuna dashboard');
} finally {
setActionInProgress(null);
}
};
const isRunning = processStatus?.is_running || selectedStudy?.status === 'running';
return (
<div className="bg-dark-800 rounded-xl border border-dark-600 overflow-hidden">
{/* Header */}
<div className="px-6 py-4 border-b border-dark-600 flex items-center justify-between">
<h2 className="text-lg font-semibold text-white flex items-center gap-2">
<Sliders className="w-5 h-5 text-primary-400" />
Optimization Control
</h2>
<button
onClick={() => setShowSettings(!showSettings)}
className={`p-2 rounded-lg transition-colors ${
showSettings ? 'bg-primary-600 text-white' : 'bg-dark-700 text-dark-300 hover:text-white'
}`}
>
<Settings className="w-4 h-4" />
</button>
</div>
{/* Status */}
<div className="px-6 py-4 border-b border-dark-700">
<div className="flex items-center justify-between">
<div>
<div className="text-sm text-dark-400 mb-1">Status</div>
<div className="flex items-center gap-2">
{isRunning ? (
<>
<div className="w-3 h-3 bg-green-500 rounded-full animate-pulse" />
<span className="text-green-400 font-medium">Running</span>
</>
) : (
<>
<div className="w-3 h-3 bg-dark-500 rounded-full" />
<span className="text-dark-400">Stopped</span>
</>
)}
</div>
</div>
{processStatus && (
<div className="text-right">
{processStatus.iteration && (
<div className="text-sm text-dark-400">
Iteration: <span className="text-white">{processStatus.iteration}</span>
</div>
)}
{processStatus.fea_count && (
<div className="text-sm text-dark-400">
FEA: <span className="text-primary-400">{processStatus.fea_count}</span>
{processStatus.nn_count && (
<> | NN: <span className="text-orange-400">{processStatus.nn_count}</span></>
)}
</div>
)}
</div>
)}
</div>
</div>
{/* Settings Panel */}
{showSettings && (
<div className="px-6 py-4 border-b border-dark-700 bg-dark-750">
<div className="grid grid-cols-3 gap-4">
<div>
<label className="block text-xs text-dark-400 mb-1">Max Iterations</label>
<input
type="number"
value={settings.maxIterations}
onChange={(e) => setSettings({ ...settings, maxIterations: parseInt(e.target.value) || 100 })}
className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-white text-sm"
/>
</div>
<div>
<label className="block text-xs text-dark-400 mb-1">FEA Batch Size</label>
<input
type="number"
value={settings.feaBatchSize}
onChange={(e) => setSettings({ ...settings, feaBatchSize: parseInt(e.target.value) || 5 })}
className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-white text-sm"
/>
</div>
<div>
<label className="block text-xs text-dark-400 mb-1">Patience</label>
<input
type="number"
value={settings.patience}
onChange={(e) => setSettings({ ...settings, patience: parseInt(e.target.value) || 5 })}
className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-white text-sm"
/>
</div>
<div>
<label className="block text-xs text-dark-400 mb-1">Tuning Trials</label>
<input
type="number"
value={settings.tuneTrials}
onChange={(e) => setSettings({ ...settings, tuneTrials: parseInt(e.target.value) || 30 })}
className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-white text-sm"
/>
</div>
<div>
<label className="block text-xs text-dark-400 mb-1">Ensemble Size</label>
<input
type="number"
value={settings.ensembleSize}
onChange={(e) => setSettings({ ...settings, ensembleSize: parseInt(e.target.value) || 3 })}
className="w-full px-3 py-2 bg-dark-700 border border-dark-600 rounded-lg text-white text-sm"
/>
</div>
<div className="flex items-end">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={settings.freshStart}
onChange={(e) => setSettings({ ...settings, freshStart: e.target.checked })}
className="w-4 h-4 rounded border-dark-600 bg-dark-700 text-primary-600"
/>
<span className="text-sm text-dark-300">Fresh Start</span>
</label>
</div>
</div>
</div>
)}
{/* Error Message */}
{error && (
<div className="px-6 py-3 bg-red-900/20 border-b border-red-800/30">
<div className="flex items-center gap-2 text-red-400 text-sm">
<AlertTriangle className="w-4 h-4" />
{error}
</div>
</div>
)}
{/* Actions */}
<div className="p-6">
<div className="grid grid-cols-2 gap-3">
{/* Start / Kill Button */}
{isRunning ? (
<button
onClick={handleStop}
disabled={actionInProgress !== null}
className="flex items-center justify-center gap-2 px-4 py-3 bg-red-600 hover:bg-red-500
disabled:opacity-50 disabled:cursor-not-allowed
text-white rounded-lg transition-colors font-medium"
title="Force kill the optimization process and all child processes"
>
{actionInProgress === 'stop' ? (
<Loader2 className="w-5 h-5 animate-spin" />
) : (
<Skull className="w-5 h-5" />
)}
Kill Process
</button>
) : (
<button
onClick={handleStart}
disabled={actionInProgress !== null}
className="flex items-center justify-center gap-2 px-4 py-3 bg-green-600 hover:bg-green-500
disabled:opacity-50 disabled:cursor-not-allowed
text-white rounded-lg transition-colors font-medium"
>
{actionInProgress === 'start' ? (
<Loader2 className="w-5 h-5 animate-spin" />
) : (
<Play className="w-5 h-5" />
)}
Start Optimization
</button>
)}
{/* Validate Button */}
<button
onClick={handleValidate}
disabled={actionInProgress !== null || isRunning}
className="flex items-center justify-center gap-2 px-4 py-3 bg-primary-600 hover:bg-primary-500
disabled:opacity-50 disabled:cursor-not-allowed
text-white rounded-lg transition-colors font-medium"
>
{actionInProgress === 'validate' ? (
<Loader2 className="w-5 h-5 animate-spin" />
) : (
<CheckCircle className="w-5 h-5" />
)}
Validate Top {validateTopN}
</button>
</div>
{/* Validation Settings */}
<div className="mt-3 flex items-center gap-2">
<span className="text-sm text-dark-400">Validate top</span>
<input
type="number"
min={1}
max={20}
value={validateTopN}
onChange={(e) => setValidateTopN(parseInt(e.target.value) || 5)}
className="w-16 px-2 py-1 bg-dark-700 border border-dark-600 rounded text-white text-sm text-center"
/>
<span className="text-sm text-dark-400">NN predictions with FEA</span>
</div>
{/* Optuna Dashboard Button */}
<button
onClick={handleLaunchOptuna}
disabled={actionInProgress !== null}
className="w-full mt-4 flex items-center justify-center gap-2 px-4 py-2
bg-dark-700 hover:bg-dark-600 border border-dark-600
disabled:opacity-50 disabled:cursor-not-allowed
text-dark-300 hover:text-white rounded-lg transition-colors text-sm"
>
{actionInProgress === 'optuna' ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<ExternalLink className="w-4 h-4" />
)}
Launch Optuna Dashboard
</button>
</div>
</div>
);
};
export default ControlPanel;

View File

@@ -1,31 +1,109 @@
import { NavLink } from 'react-router-dom';
import { LayoutDashboard, Settings, FileText, Activity } from 'lucide-react';
import { NavLink, useNavigate } from 'react-router-dom';
import {
Home,
Activity,
FileText,
BarChart3,
ChevronLeft,
Play,
CheckCircle,
Clock,
Zap
} from 'lucide-react';
import clsx from 'clsx';
import { useStudy } from '../../context/StudyContext';
export const Sidebar = () => {
const navItems = [
{ to: '/dashboard', icon: Activity, label: 'Live Dashboard' },
{ to: '/configurator', icon: Settings, label: 'Configurator' },
{ to: '/results', icon: FileText, label: 'Results Viewer' },
];
const { selectedStudy, clearStudy } = useStudy();
const navigate = useNavigate();
const handleBackToHome = () => {
clearStudy();
navigate('/');
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'running':
return <Play className="w-3 h-3 text-green-400" />;
case 'completed':
return <CheckCircle className="w-3 h-3 text-blue-400" />;
default:
return <Clock className="w-3 h-3 text-dark-400" />;
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'running':
return 'text-green-400';
case 'completed':
return 'text-blue-400';
default:
return 'text-dark-400';
}
};
// Navigation items depend on whether a study is selected
const navItems = selectedStudy
? [
{ to: '/dashboard', icon: Activity, label: 'Live Tracker' },
{ to: '/results', icon: FileText, label: 'Reports' },
{ to: '/analytics', icon: BarChart3, label: 'Analytics' },
]
: [
{ to: '/', icon: Home, label: 'Select Study' },
];
return (
<aside className="w-64 bg-dark-800 border-r border-dark-600 flex flex-col h-screen fixed left-0 top-0">
{/* Header */}
<div className="p-6 border-b border-dark-600">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-primary-600 rounded-lg flex items-center justify-center">
<LayoutDashboard className="w-5 h-5 text-white" />
<Zap className="w-5 h-5 text-white" />
</div>
<h1 className="text-xl font-bold text-white tracking-tight">Atomizer</h1>
</div>
<p className="text-xs text-dark-300 mt-1 ml-11">Optimization Platform</p>
</div>
{/* Selected Study Info */}
{selectedStudy && (
<div className="p-4 border-b border-dark-600">
<button
onClick={handleBackToHome}
className="flex items-center gap-2 text-sm text-dark-400 hover:text-white
transition-colors mb-3 group"
>
<ChevronLeft className="w-4 h-4 group-hover:-translate-x-1 transition-transform" />
Change Study
</button>
<div className="bg-dark-700 rounded-lg p-3">
<div className="text-xs font-medium text-dark-400 uppercase mb-1">Active Study</div>
<div className="text-white font-medium truncate">{selectedStudy.name}</div>
<div className="flex items-center gap-2 mt-2">
<span className={`flex items-center gap-1 text-xs ${getStatusColor(selectedStudy.status)}`}>
{getStatusIcon(selectedStudy.status)}
{selectedStudy.status}
</span>
<span className="text-dark-500">|</span>
<span className="text-xs text-dark-400">
{selectedStudy.progress.current}/{selectedStudy.progress.total}
</span>
</div>
</div>
</div>
)}
{/* Navigation */}
<nav className="flex-1 p-4 space-y-1">
{navItems.map((item) => (
<NavLink
key={item.to}
to={item.to}
end={item.to === '/'}
className={({ isActive }) =>
clsx(
'flex items-center gap-3 px-4 py-3 rounded-lg transition-colors duration-200',
@@ -41,6 +119,7 @@ export const Sidebar = () => {
))}
</nav>
{/* Footer Status */}
<div className="p-4 border-t border-dark-600">
<div className="bg-dark-700 rounded-lg p-4">
<div className="text-xs font-medium text-dark-400 uppercase mb-2">System Status</div>
@@ -48,8 +127,14 @@ export const Sidebar = () => {
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
Backend Online
</div>
{selectedStudy && selectedStudy.status === 'running' && (
<div className="flex items-center gap-2 text-sm text-green-400 mt-1">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
Optimization Running
</div>
)}
</div>
</div>
</aside>
);
};
};

View File

@@ -0,0 +1,93 @@
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { Study } from '../types';
import { apiClient } from '../api/client';
interface StudyContextType {
selectedStudy: Study | null;
setSelectedStudy: (study: Study | null) => void;
studies: Study[];
refreshStudies: () => Promise<void>;
isLoading: boolean;
clearStudy: () => void;
}
const StudyContext = createContext<StudyContextType | undefined>(undefined);
export const StudyProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [selectedStudy, setSelectedStudyState] = useState<Study | null>(null);
const [studies, setStudies] = useState<Study[]>([]);
const [isLoading, setIsLoading] = useState(true);
const refreshStudies = async () => {
try {
setIsLoading(true);
const response = await apiClient.getStudies();
setStudies(response.studies);
// If we have a selected study, refresh its data
if (selectedStudy) {
const updated = response.studies.find(s => s.id === selectedStudy.id);
if (updated) {
setSelectedStudyState(updated);
}
}
} catch (error) {
console.error('Failed to fetch studies:', error);
} finally {
setIsLoading(false);
}
};
const setSelectedStudy = (study: Study | null) => {
setSelectedStudyState(study);
if (study) {
localStorage.setItem('selectedStudyId', study.id);
} else {
localStorage.removeItem('selectedStudyId');
}
};
const clearStudy = () => {
setSelectedStudyState(null);
localStorage.removeItem('selectedStudyId');
};
// Initial load
useEffect(() => {
const init = async () => {
await refreshStudies();
// Restore last selected study
const lastStudyId = localStorage.getItem('selectedStudyId');
if (lastStudyId) {
const response = await apiClient.getStudies();
const study = response.studies.find(s => s.id === lastStudyId);
if (study) {
setSelectedStudyState(study);
}
}
};
init();
}, []);
return (
<StudyContext.Provider value={{
selectedStudy,
setSelectedStudy,
studies,
refreshStudies,
isLoading,
clearStudy
}}>
{children}
</StudyContext.Provider>
);
};
export const useStudy = () => {
const context = useContext(StudyContext);
if (context === undefined) {
throw new Error('useStudy must be used within a StudyProvider');
}
return context;
};

View File

@@ -1,3 +1,5 @@
@import 'katex/dist/katex.min.css';
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -1,14 +1,17 @@
import { useState, useEffect, lazy, Suspense } from 'react';
import { useNavigate } from 'react-router-dom';
import {
LineChart, Line, ScatterChart, Scatter,
XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, Cell
} from 'recharts';
import { Terminal } from 'lucide-react';
import { useOptimizationWebSocket } from '../hooks/useWebSocket';
import { apiClient } from '../api/client';
import { useStudy } from '../context/StudyContext';
import { Card } from '../components/common/Card';
import { MetricCard } from '../components/dashboard/MetricCard';
import { StudyCard } from '../components/dashboard/StudyCard';
// import { OptimizerPanel } from '../components/OptimizerPanel'; // Not used currently
import { ControlPanel } from '../components/dashboard/ControlPanel';
import { ClaudeTerminal } from '../components/ClaudeTerminal';
import { ParetoPlot } from '../components/ParetoPlot';
import { ParallelCoordinatesPlot } from '../components/ParallelCoordinatesPlot';
import { ParameterImportanceChart } from '../components/ParameterImportanceChart';
@@ -16,7 +19,7 @@ import { ConvergencePlot } from '../components/ConvergencePlot';
import { StudyReportViewer } from '../components/StudyReportViewer';
import { ConsoleOutput } from '../components/ConsoleOutput';
import { ExpandableChart } from '../components/ExpandableChart';
import type { Study, Trial, ConvergenceDataPoint, ParameterSpaceDataPoint } from '../types';
import type { Trial, ConvergenceDataPoint, ParameterSpaceDataPoint } from '../types';
// Lazy load Plotly components for better initial load performance
const PlotlyParallelCoordinates = lazy(() => import('../components/plotly/PlotlyParallelCoordinates').then(m => ({ default: m.PlotlyParallelCoordinates })));
@@ -32,8 +35,17 @@ const ChartLoading = () => (
);
export default function Dashboard() {
const [studies, setStudies] = useState<Study[]>([]);
const [selectedStudyId, setSelectedStudyId] = useState<string | null>(null);
const navigate = useNavigate();
const { selectedStudy, refreshStudies } = useStudy();
const selectedStudyId = selectedStudy?.id || null;
// Redirect to home if no study selected
useEffect(() => {
if (!selectedStudy) {
navigate('/');
}
}, [selectedStudy, navigate]);
const [allTrials, setAllTrials] = useState<Trial[]>([]);
const [displayedTrials, setDisplayedTrials] = useState<Trial[]>([]);
const [bestValue, setBestValue] = useState<number>(Infinity);
@@ -55,26 +67,9 @@ export default function Dashboard() {
// Chart library toggle: 'recharts' (faster) or 'plotly' (more interactive but slower)
const [chartLibrary, setChartLibrary] = useState<'plotly' | 'recharts'>('recharts');
// Load studies on mount
useEffect(() => {
apiClient.getStudies()
.then(data => {
setStudies(data.studies);
if (data.studies.length > 0) {
// Check LocalStorage for last selected study
const savedStudyId = localStorage.getItem('lastSelectedStudyId');
const studyExists = data.studies.find(s => s.id === savedStudyId);
if (savedStudyId && studyExists) {
setSelectedStudyId(savedStudyId);
} else {
const running = data.studies.find(s => s.status === 'running');
setSelectedStudyId(running?.id || data.studies[0].id);
}
}
})
.catch(console.error);
}, []);
// Claude chat panel state
const [chatOpen, setChatOpen] = useState(false);
const [chatExpanded, setChatExpanded] = useState(false);
const showAlert = (type: 'success' | 'warning', message: string) => {
const id = alertIdCounter;
@@ -111,9 +106,6 @@ export default function Dashboard() {
setPrunedCount(0);
setExpandedTrials(new Set());
// Save to LocalStorage
localStorage.setItem('lastSelectedStudyId', selectedStudyId);
apiClient.getStudyHistory(selectedStudyId)
.then(data => {
const validTrials = data.trials.filter(t => t.objective !== null && t.objective !== undefined);
@@ -331,6 +323,19 @@ export default function Dashboard() {
<p className="text-dark-300 mt-1">Real-time optimization monitoring</p>
</div>
<div className="flex gap-2">
{/* Claude Code Terminal Toggle Button */}
<button
onClick={() => setChatOpen(!chatOpen)}
className={`flex items-center gap-2 px-4 py-2 rounded-lg transition-colors ${
chatOpen
? 'bg-primary-600 text-white'
: 'bg-dark-700 text-dark-200 hover:bg-dark-600 hover:text-white border border-dark-600'
}`}
title="Open Claude Code terminal"
>
<Terminal className="w-4 h-4" />
<span className="hidden sm:inline">Claude Code</span>
</button>
{selectedStudyId && (
<StudyReportViewer studyId={selectedStudyId} />
)}
@@ -380,24 +385,13 @@ export default function Dashboard() {
</header>
<div className="grid grid-cols-12 gap-6">
{/* Sidebar - Study List */}
{/* Control Panel - Left Sidebar */}
<aside className="col-span-3">
<Card title="Active Studies">
<div className="space-y-3 max-h-[calc(100vh-200px)] overflow-y-auto">
{studies.map(study => (
<StudyCard
key={study.id}
study={study}
isActive={study.id === selectedStudyId}
onClick={() => setSelectedStudyId(study.id)}
/>
))}
</div>
</Card>
<ControlPanel onStatusChange={refreshStudies} />
</aside>
{/* Main Content */}
<main className="col-span-9">
{/* Main Content - shrinks when chat is open */}
<main className={chatOpen ? 'col-span-5' : 'col-span-9'}>
{/* Study Name Header */}
{selectedStudyId && (
<div className="mb-4 pb-3 border-b border-dark-600">
@@ -884,6 +878,17 @@ export default function Dashboard() {
/>
</div>
</main>
{/* Claude Code Terminal - Right Sidebar */}
{chatOpen && (
<aside className="col-span-4 h-[calc(100vh-12rem)] sticky top-24">
<ClaudeTerminal
isExpanded={chatExpanded}
onToggleExpand={() => setChatExpanded(!chatExpanded)}
onClose={() => setChatOpen(false)}
/>
</aside>
)}
</div>
</div>
);

View File

@@ -0,0 +1,455 @@
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
FolderOpen,
Play,
CheckCircle,
Clock,
AlertCircle,
ArrowRight,
RefreshCw,
Zap,
FileText,
ChevronDown,
ChevronUp,
Target,
Activity
} from 'lucide-react';
import { useStudy } from '../context/StudyContext';
import { Study } from '../types';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import rehypeKatex from 'rehype-katex';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
import { apiClient } from '../api/client';
const Home: React.FC = () => {
const { studies, setSelectedStudy, refreshStudies, isLoading } = useStudy();
const [selectedPreview, setSelectedPreview] = useState<Study | null>(null);
const [readme, setReadme] = useState<string>('');
const [readmeLoading, setReadmeLoading] = useState(false);
const [showAllStudies, setShowAllStudies] = useState(false);
const navigate = useNavigate();
// Load README when a study is selected for preview
useEffect(() => {
if (selectedPreview) {
loadReadme(selectedPreview.id);
} else {
setReadme('');
}
}, [selectedPreview]);
const loadReadme = async (studyId: string) => {
setReadmeLoading(true);
try {
const response = await apiClient.getStudyReadme(studyId);
setReadme(response.content || 'No README found for this study.');
} catch (error) {
setReadme('No README found for this study.');
} finally {
setReadmeLoading(false);
}
};
const handleSelectStudy = (study: Study) => {
setSelectedStudy(study);
navigate('/dashboard');
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'running':
return <Play className="w-3.5 h-3.5" />;
case 'completed':
return <CheckCircle className="w-3.5 h-3.5" />;
case 'not_started':
return <Clock className="w-3.5 h-3.5" />;
default:
return <AlertCircle className="w-3.5 h-3.5" />;
}
};
const getStatusStyles = (status: string) => {
switch (status) {
case 'running':
return {
badge: 'bg-green-500/20 text-green-400 border-green-500/30',
card: 'border-green-500/30 hover:border-green-500/50',
glow: 'shadow-green-500/10'
};
case 'completed':
return {
badge: 'bg-blue-500/20 text-blue-400 border-blue-500/30',
card: 'border-blue-500/30 hover:border-blue-500/50',
glow: 'shadow-blue-500/10'
};
case 'not_started':
return {
badge: 'bg-dark-600 text-dark-400 border-dark-500',
card: 'border-dark-600 hover:border-dark-500',
glow: ''
};
default:
return {
badge: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30',
card: 'border-yellow-500/30 hover:border-yellow-500/50',
glow: 'shadow-yellow-500/10'
};
}
};
// Sort studies: running first, then by trial count
const sortedStudies = [...studies].sort((a, b) => {
if (a.status === 'running' && b.status !== 'running') return -1;
if (b.status === 'running' && a.status !== 'running') return 1;
return b.progress.current - a.progress.current;
});
const displayedStudies = showAllStudies ? sortedStudies : sortedStudies.slice(0, 6);
return (
<div className="min-h-screen bg-dark-900">
{/* Header */}
<header className="bg-dark-800/50 border-b border-dark-700 backdrop-blur-sm sticky top-0 z-10">
<div className="max-w-[1600px] mx-auto px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="w-11 h-11 bg-gradient-to-br from-primary-500 to-primary-700 rounded-xl flex items-center justify-center shadow-lg shadow-primary-500/20">
<Zap className="w-6 h-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold text-white">Atomizer</h1>
<p className="text-dark-400 text-sm">FEA Optimization Platform</p>
</div>
</div>
<button
onClick={() => refreshStudies()}
disabled={isLoading}
className="flex items-center gap-2 px-4 py-2 bg-dark-700 hover:bg-dark-600
text-white rounded-lg transition-all disabled:opacity-50 border border-dark-600"
>
<RefreshCw className={`w-4 h-4 ${isLoading ? 'animate-spin' : ''}`} />
Refresh
</button>
</div>
</div>
</header>
<main className="max-w-[1600px] mx-auto px-6 py-8">
{/* Study Selection Section */}
<section className="mb-8">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-white flex items-center gap-2">
<FolderOpen className="w-5 h-5 text-primary-400" />
Select a Study
</h2>
{studies.length > 6 && (
<button
onClick={() => setShowAllStudies(!showAllStudies)}
className="text-sm text-primary-400 hover:text-primary-300 flex items-center gap-1"
>
{showAllStudies ? (
<>Show Less <ChevronUp className="w-4 h-4" /></>
) : (
<>Show All ({studies.length}) <ChevronDown className="w-4 h-4" /></>
)}
</button>
)}
</div>
{isLoading ? (
<div className="flex items-center justify-center py-12 text-dark-400">
<RefreshCw className="w-6 h-6 animate-spin mr-3" />
Loading studies...
</div>
) : studies.length === 0 ? (
<div className="text-center py-12 text-dark-400">
<FolderOpen className="w-12 h-12 mx-auto mb-3 opacity-50" />
<p>No studies found. Create a new study to get started.</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{displayedStudies.map((study) => {
const styles = getStatusStyles(study.status);
const isSelected = selectedPreview?.id === study.id;
return (
<div
key={study.id}
onClick={() => setSelectedPreview(study)}
className={`
relative p-4 rounded-xl border cursor-pointer transition-all duration-200
bg-dark-800 hover:bg-dark-750
${styles.card} ${styles.glow}
${isSelected ? 'ring-2 ring-primary-500 border-primary-500' : ''}
`}
>
{/* Status Badge */}
<div className="flex items-start justify-between mb-3">
<div className="flex-1 min-w-0 pr-2">
<h3 className="text-white font-medium truncate">{study.name || study.id}</h3>
<p className="text-dark-500 text-xs truncate mt-0.5">{study.id}</p>
</div>
<span className={`flex items-center gap-1.5 px-2 py-1 text-xs font-medium rounded-full border ${styles.badge}`}>
{getStatusIcon(study.status)}
{study.status}
</span>
</div>
{/* Stats */}
<div className="flex items-center gap-4 text-sm mb-3">
<div className="flex items-center gap-1.5 text-dark-400">
<Activity className="w-3.5 h-3.5" />
<span>{study.progress.current} trials</span>
</div>
{study.best_value !== null && (
<div className="flex items-center gap-1.5 text-primary-400">
<Target className="w-3.5 h-3.5" />
<span>{study.best_value.toFixed(4)}</span>
</div>
)}
</div>
{/* Progress Bar */}
<div className="h-1.5 bg-dark-700 rounded-full overflow-hidden">
<div
className={`h-full rounded-full transition-all duration-500 ${
study.status === 'running' ? 'bg-green-500' :
study.status === 'completed' ? 'bg-blue-500' : 'bg-primary-500'
}`}
style={{ width: `${Math.min((study.progress.current / study.progress.total) * 100, 100)}%` }}
/>
</div>
{/* Selected Indicator */}
{isSelected && (
<div className="absolute -bottom-px left-1/2 -translate-x-1/2 w-12 h-1 bg-primary-500 rounded-t-full" />
)}
</div>
);
})}
</div>
)}
</section>
{/* Study Documentation Section */}
{selectedPreview && (
<section className="animate-in fade-in slide-in-from-bottom-4 duration-300">
{/* Documentation Header */}
<div className="bg-dark-800 rounded-t-xl border border-dark-600 border-b-0">
<div className="px-6 py-4 flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="w-10 h-10 bg-dark-700 rounded-lg flex items-center justify-center">
<FileText className="w-5 h-5 text-primary-400" />
</div>
<div>
<h2 className="text-xl font-semibold text-white">{selectedPreview.name || selectedPreview.id}</h2>
<p className="text-dark-400 text-sm">Study Documentation</p>
</div>
</div>
<button
onClick={() => handleSelectStudy(selectedPreview)}
className="flex items-center gap-2 px-5 py-2.5 bg-primary-600 hover:bg-primary-500
text-white rounded-lg transition-all font-medium shadow-lg shadow-primary-500/20
hover:shadow-primary-500/30"
>
Open Dashboard
<ArrowRight className="w-4 h-4" />
</button>
</div>
</div>
{/* README Content */}
<div className="bg-dark-850 rounded-b-xl border border-dark-600 border-t-0 overflow-hidden">
{readmeLoading ? (
<div className="flex items-center justify-center py-16 text-dark-400">
<RefreshCw className="w-6 h-6 animate-spin mr-3" />
Loading documentation...
</div>
) : (
<div className="p-8 overflow-x-auto">
<article className="markdown-body max-w-none">
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeKatex]}
components={{
// Custom heading styles
h1: ({ children }) => (
<h1 className="text-3xl font-bold text-white mb-6 pb-3 border-b border-dark-600">
{children}
</h1>
),
h2: ({ children }) => (
<h2 className="text-2xl font-semibold text-white mt-10 mb-4 pb-2 border-b border-dark-700">
{children}
</h2>
),
h3: ({ children }) => (
<h3 className="text-xl font-semibold text-white mt-8 mb-3">
{children}
</h3>
),
h4: ({ children }) => (
<h4 className="text-lg font-medium text-white mt-6 mb-2">
{children}
</h4>
),
// Paragraphs
p: ({ children }) => (
<p className="text-dark-300 leading-relaxed mb-4">
{children}
</p>
),
// Strong/Bold
strong: ({ children }) => (
<strong className="text-white font-semibold">{children}</strong>
),
// Links
a: ({ href, children }) => (
<a
href={href}
className="text-primary-400 hover:text-primary-300 underline underline-offset-2"
target="_blank"
rel="noopener noreferrer"
>
{children}
</a>
),
// Lists
ul: ({ children }) => (
<ul className="list-disc list-inside text-dark-300 mb-4 space-y-1.5 ml-2">
{children}
</ul>
),
ol: ({ children }) => (
<ol className="list-decimal list-inside text-dark-300 mb-4 space-y-1.5 ml-2">
{children}
</ol>
),
li: ({ children }) => (
<li className="text-dark-300 leading-relaxed">{children}</li>
),
// Code blocks with syntax highlighting
code: ({ inline, className, children, ...props }: any) => {
const match = /language-(\w+)/.exec(className || '');
const language = match ? match[1] : '';
if (!inline && language) {
return (
<div className="my-4 rounded-lg overflow-hidden border border-dark-600">
<div className="bg-dark-700 px-4 py-2 text-xs text-dark-400 font-mono border-b border-dark-600">
{language}
</div>
<SyntaxHighlighter
style={oneDark}
language={language}
PreTag="div"
customStyle={{
margin: 0,
padding: '1rem',
background: '#1a1d23',
fontSize: '0.875rem',
}}
{...props}
>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
</div>
);
}
if (!inline) {
return (
<pre className="my-4 p-4 bg-dark-700 rounded-lg border border-dark-600 overflow-x-auto">
<code className="text-primary-400 text-sm font-mono">{children}</code>
</pre>
);
}
return (
<code className="px-1.5 py-0.5 bg-dark-700 text-primary-400 rounded text-sm font-mono">
{children}
</code>
);
},
// Tables
table: ({ children }) => (
<div className="my-6 overflow-x-auto rounded-lg border border-dark-600">
<table className="w-full text-sm">
{children}
</table>
</div>
),
thead: ({ children }) => (
<thead className="bg-dark-700 text-white">
{children}
</thead>
),
tbody: ({ children }) => (
<tbody className="divide-y divide-dark-600">
{children}
</tbody>
),
tr: ({ children }) => (
<tr className="hover:bg-dark-750 transition-colors">
{children}
</tr>
),
th: ({ children }) => (
<th className="px-4 py-3 text-left font-semibold text-white border-b border-dark-600">
{children}
</th>
),
td: ({ children }) => (
<td className="px-4 py-3 text-dark-300">
{children}
</td>
),
// Blockquotes
blockquote: ({ children }) => (
<blockquote className="my-4 pl-4 border-l-4 border-primary-500 bg-dark-750 py-3 pr-4 rounded-r-lg">
{children}
</blockquote>
),
// Horizontal rules
hr: () => (
<hr className="my-8 border-dark-600" />
),
// Images
img: ({ src, alt }) => (
<img
src={src}
alt={alt}
className="my-4 rounded-lg max-w-full h-auto border border-dark-600"
/>
),
}}
>
{readme}
</ReactMarkdown>
</article>
</div>
)}
</div>
</section>
)}
{/* Empty State when no study selected */}
{!selectedPreview && studies.length > 0 && (
<section className="flex items-center justify-center py-16 text-dark-400">
<div className="text-center">
<FileText className="w-16 h-16 mx-auto mb-4 opacity-30" />
<p className="text-lg">Select a study to view its documentation</p>
<p className="text-sm mt-1 text-dark-500">Click on any study card above</p>
</div>
</section>
)}
</main>
</div>
);
};
export default Home;

View File

@@ -1,151 +1,242 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { Card } from '../components/common/Card';
import { Button } from '../components/common/Button';
import { Download, FileText, Image, RefreshCw } from 'lucide-react';
import {
Download,
FileText,
RefreshCw,
Sparkles,
Loader2,
AlertTriangle,
CheckCircle,
Copy
} from 'lucide-react';
import { apiClient } from '../api/client';
import { Study } from '../types';
import { useStudy } from '../context/StudyContext';
import ReactMarkdown from 'react-markdown';
export default function Results() {
const [studies, setStudies] = useState<Study[]>([]);
const [selectedStudyId, setSelectedStudyId] = useState<string | null>(null);
const { selectedStudy } = useStudy();
const navigate = useNavigate();
const [reportContent, setReportContent] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [generating, setGenerating] = useState(false);
const [error, setError] = useState<string | null>(null);
const [copied, setCopied] = useState(false);
const [lastGenerated, setLastGenerated] = useState<string | null>(null);
// Redirect if no study selected
useEffect(() => {
apiClient.getStudies()
.then(data => {
setStudies(data.studies);
if (data.studies.length > 0) {
const completed = data.studies.find(s => s.status === 'completed');
setSelectedStudyId(completed?.id || data.studies[0].id);
}
})
.catch(console.error);
}, []);
useEffect(() => {
if (selectedStudyId) {
setLoading(true);
apiClient.getStudyReport(selectedStudyId)
.then(data => {
setReportContent(data.content);
setLoading(false);
})
.catch(err => {
console.error('Failed to fetch report:', err);
// Fallback for demo if report doesn't exist
setReportContent(`# Optimization Report: ${selectedStudyId}
## Executive Summary
The optimization study successfully converged after 45 trials. The best design achieved a mass reduction of 15% while maintaining all constraints.
## Key Findings
- **Best Objective Value**: 115.185 Hz
- **Critical Parameter**: Plate Thickness (sensitivity: 0.85)
- **Constraint Margins**: All safety factors > 1.2
## Recommendations
Based on the results, we recommend proceeding with the design from Trial #45. Further refinement could be achieved by narrowing the bounds for 'thickness'.
`);
setLoading(false);
});
if (!selectedStudy) {
navigate('/');
}
}, [selectedStudyId]);
}, [selectedStudy, navigate]);
// Load report when study changes
useEffect(() => {
if (selectedStudy) {
loadReport();
}
}, [selectedStudy]);
const loadReport = async () => {
if (!selectedStudy) return;
const handleRegenerate = () => {
if (!selectedStudyId) return;
setLoading(true);
// In a real app, this would call an endpoint to trigger report generation
setTimeout(() => {
setError(null);
try {
const data = await apiClient.getStudyReport(selectedStudy.id);
setReportContent(data.content);
if (data.generated_at) {
setLastGenerated(data.generated_at);
}
} catch (err: any) {
// No report yet - show placeholder
setReportContent(null);
} finally {
setLoading(false);
}, 2000);
}
};
const handleGenerate = async () => {
if (!selectedStudy) return;
setGenerating(true);
setError(null);
try {
const data = await apiClient.generateReport(selectedStudy.id);
setReportContent(data.content);
if (data.generated_at) {
setLastGenerated(data.generated_at);
}
} catch (err: any) {
setError(err.message || 'Failed to generate report');
} finally {
setGenerating(false);
}
};
const handleCopy = async () => {
if (reportContent) {
await navigator.clipboard.writeText(reportContent);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
const handleDownload = () => {
if (!reportContent || !selectedStudy) return;
const blob = new Blob([reportContent], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${selectedStudy.id}_report.md`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
if (!selectedStudy) {
return null;
}
return (
<div className="container mx-auto h-[calc(100vh-100px)] flex flex-col">
<div className="h-full flex flex-col">
{/* Header */}
<header className="mb-6 flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-primary-400">Results Viewer</h1>
<p className="text-dark-300 mt-1">Analyze completed optimization studies</p>
<h1 className="text-2xl font-bold text-white">Optimization Report</h1>
<p className="text-dark-400 mt-1">{selectedStudy.name}</p>
</div>
<div className="flex gap-2">
<Button
variant="secondary"
icon={<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />}
onClick={handleRegenerate}
disabled={loading || !selectedStudyId}
<Button
variant="primary"
icon={generating ? <Loader2 className="w-4 h-4 animate-spin" /> : <Sparkles className="w-4 h-4" />}
onClick={handleGenerate}
disabled={generating}
>
Regenerate
</Button>
<Button variant="secondary" icon={<Download className="w-4 h-4" />}>
Export Data
{generating ? 'Generating...' : reportContent ? 'Update Report' : 'Generate Report'}
</Button>
{reportContent && (
<>
<Button
variant="secondary"
icon={copied ? <CheckCircle className="w-4 h-4 text-green-400" /> : <Copy className="w-4 h-4" />}
onClick={handleCopy}
>
{copied ? 'Copied!' : 'Copy'}
</Button>
<Button
variant="secondary"
icon={<Download className="w-4 h-4" />}
onClick={handleDownload}
>
Download
</Button>
</>
)}
</div>
</header>
<div className="grid grid-cols-12 gap-6 flex-1 min-h-0">
{/* Sidebar - Study Selection */}
<aside className="col-span-3 flex flex-col gap-4">
<Card title="Select Study" className="flex-1 overflow-hidden flex flex-col">
<div className="space-y-2 overflow-y-auto flex-1 pr-2">
{studies.map(study => (
<button
key={study.id}
onClick={() => setSelectedStudyId(study.id)}
className={`w-full text-left p-3 rounded-lg transition-colors ${
selectedStudyId === study.id
? 'bg-primary-900/30 text-primary-100 border border-primary-700/50'
: 'text-dark-300 hover:bg-dark-700'
}`}
>
<div className="font-medium truncate">{study.name}</div>
<div className="text-xs text-dark-400 mt-1 capitalize">{study.status}</div>
</button>
))}
</div>
</Card>
</aside>
{/* Error Message */}
{error && (
<div className="mb-4 p-4 bg-red-900/20 border border-red-800/30 rounded-lg">
<div className="flex items-center gap-2 text-red-400">
<AlertTriangle className="w-5 h-5" />
<span>{error}</span>
</div>
</div>
)}
{/* Main Content - Report Viewer */}
<main className="col-span-9 flex flex-col gap-6 overflow-hidden">
<Card className="flex-1 overflow-hidden flex flex-col">
<div className="flex items-center justify-between border-b border-dark-600 pb-4 mb-4">
<h2 className="text-xl font-semibold text-white flex items-center gap-2">
<FileText className="w-5 h-5 text-primary-400" />
Optimization Report
</h2>
<div className="flex gap-2">
<button className="p-2 text-dark-300 hover:text-white hover:bg-dark-700 rounded-lg" title="View Charts">
<Image className="w-5 h-5" />
</button>
{/* Main Content */}
<div className="flex-1 min-h-0">
<Card className="h-full overflow-hidden flex flex-col">
<div className="flex items-center justify-between border-b border-dark-600 pb-4 mb-4">
<h2 className="text-lg font-semibold text-white flex items-center gap-2">
<FileText className="w-5 h-5 text-primary-400" />
Report Content
</h2>
{lastGenerated && (
<span className="text-xs text-dark-400">
Last generated: {new Date(lastGenerated).toLocaleString()}
</span>
)}
</div>
<div className="flex-1 overflow-y-auto pr-4 custom-scrollbar">
{loading ? (
<div className="h-full flex flex-col items-center justify-center text-dark-300">
<RefreshCw className="w-8 h-8 animate-spin mb-3" />
<span>Loading report...</span>
</div>
</div>
<div className="flex-1 overflow-y-auto pr-4 custom-scrollbar">
{loading ? (
<div className="h-full flex items-center justify-center text-dark-300">
<RefreshCw className="w-8 h-8 animate-spin mb-2" />
<span className="ml-2">Loading report...</span>
</div>
) : reportContent ? (
<div className="prose prose-invert max-w-none">
{/* Simple markdown rendering for now */}
{reportContent.split('\n').map((line, i) => {
if (line.startsWith('# ')) return <h1 key={i} className="text-2xl font-bold text-white mt-6 mb-4">{line.substring(2)}</h1>;
if (line.startsWith('## ')) return <h2 key={i} className="text-xl font-bold text-primary-200 mt-6 mb-3">{line.substring(3)}</h2>;
if (line.startsWith('- ')) return <li key={i} className="ml-4 text-dark-100">{line.substring(2)}</li>;
return <p key={i} className="text-dark-200 mb-2">{line}</p>;
})}
</div>
) : (
<div className="h-full flex items-center justify-center text-dark-300">
Select a study to view results
</div>
)}
</div>
</Card>
</main>
) : reportContent ? (
<div className="prose prose-invert prose-sm max-w-none
prose-headings:text-white prose-headings:font-semibold
prose-p:text-dark-300 prose-strong:text-white
prose-code:text-primary-400 prose-code:bg-dark-700 prose-code:px-1 prose-code:rounded
prose-pre:bg-dark-700 prose-pre:border prose-pre:border-dark-600
prose-a:text-primary-400 prose-a:no-underline hover:prose-a:underline
prose-ul:text-dark-300 prose-ol:text-dark-300
prose-li:text-dark-300
prose-table:border-collapse prose-th:border prose-th:border-dark-600 prose-th:p-2 prose-th:bg-dark-700
prose-td:border prose-td:border-dark-600 prose-td:p-2
prose-hr:border-dark-600">
<ReactMarkdown>{reportContent}</ReactMarkdown>
</div>
) : (
<div className="h-full flex flex-col items-center justify-center text-dark-400">
<FileText className="w-16 h-16 mb-4 opacity-50" />
<h3 className="text-lg font-medium text-dark-300 mb-2">No Report Generated</h3>
<p className="text-sm text-center mb-6 max-w-md">
Click "Generate Report" to create an AI-generated analysis of your optimization results.
</p>
<Button
variant="primary"
icon={<Sparkles className="w-4 h-4" />}
onClick={handleGenerate}
disabled={generating}
>
Generate Report
</Button>
</div>
)}
</div>
</Card>
</div>
{/* Study Stats */}
<div className="mt-4 grid grid-cols-4 gap-4">
<div className="bg-dark-800 rounded-lg p-4 border border-dark-600">
<div className="text-xs text-dark-400 uppercase mb-1">Total Trials</div>
<div className="text-2xl font-bold text-white">{selectedStudy.progress.current}</div>
</div>
<div className="bg-dark-800 rounded-lg p-4 border border-dark-600">
<div className="text-xs text-dark-400 uppercase mb-1">Best Value</div>
<div className="text-2xl font-bold text-primary-400">
{selectedStudy.best_value?.toFixed(4) || 'N/A'}
</div>
</div>
<div className="bg-dark-800 rounded-lg p-4 border border-dark-600">
<div className="text-xs text-dark-400 uppercase mb-1">Target</div>
<div className="text-2xl font-bold text-dark-300">
{selectedStudy.target?.toFixed(4) || 'N/A'}
</div>
</div>
<div className="bg-dark-800 rounded-lg p-4 border border-dark-600">
<div className="text-xs text-dark-400 uppercase mb-1">Status</div>
<div className={`text-lg font-bold capitalize ${
selectedStudy.status === 'completed' ? 'text-green-400' :
selectedStudy.status === 'running' ? 'text-blue-400' : 'text-dark-400'
}`}>
{selectedStudy.status}
</div>
</div>
</div>
</div>
);
}
}