diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 03ec2a10..ff837866 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -15,7 +15,39 @@ "Bash(C:UsersAntoineminiconda3envsatomizerpython.exe run_adaptive_mirror_optimization.py --fea-budget 100 --batch-size 5 --strategy hybrid)", "Bash(/c/Users/Antoine/miniconda3/envs/atomizer/python.exe:*)", "Bash(npm run build:*)", - "Bash(npm uninstall:*)" + "Bash(npm uninstall:*)", + "Bash(netstat:*)", + "Bash(findstr:*)", + "Bash(curl:*)", + "Bash(npx tsc:*)", + "Bash(atomizer-dashboard/README.md )", + "Bash(atomizer-dashboard/backend/api/main.py )", + "Bash(atomizer-dashboard/backend/api/routes/optimization.py )", + "Bash(atomizer-dashboard/backend/api/routes/claude.py )", + "Bash(atomizer-dashboard/backend/api/routes/terminal.py )", + "Bash(atomizer-dashboard/backend/api/services/ )", + "Bash(atomizer-dashboard/backend/requirements.txt )", + "Bash(atomizer-dashboard/frontend/package.json )", + "Bash(atomizer-dashboard/frontend/package-lock.json )", + "Bash(atomizer-dashboard/frontend/src/components/ClaudeChat.tsx )", + "Bash(atomizer-dashboard/frontend/src/components/ClaudeTerminal.tsx )", + "Bash(atomizer-dashboard/frontend/src/components/dashboard/ControlPanel.tsx )", + "Bash(atomizer-dashboard/frontend/src/pages/Dashboard.tsx )", + "Bash(atomizer-dashboard/frontend/src/context/ )", + "Bash(atomizer-dashboard/frontend/src/pages/Home.tsx )", + "Bash(atomizer-dashboard/frontend/src/App.tsx )", + "Bash(atomizer-dashboard/frontend/src/api/client.ts )", + "Bash(atomizer-dashboard/frontend/src/components/layout/Sidebar.tsx )", + "Bash(atomizer-dashboard/frontend/src/index.css )", + "Bash(atomizer-dashboard/frontend/src/pages/Results.tsx )", + "Bash(atomizer-dashboard/frontend/tailwind.config.js )", + "Bash(docs/07_DEVELOPMENT/DASHBOARD_IMPROVEMENT_PLAN.md)", + "Bash(taskkill:*)", + "Bash(xargs:*)", + "Bash(cmd.exe /c:*)", + "Bash(powershell.exe -Command:*)", + "Bash(where:*)", + "Bash(type %USERPROFILE%.claude*)" ], "deny": [], "ask": [] diff --git a/.claude/skills/create-study.md b/.claude/skills/create-study.md index a4ea81c6..99904e55 100644 --- a/.claude/skills/create-study.md +++ b/.claude/skills/create-study.md @@ -1,7 +1,7 @@ # Create Optimization Study Skill -**Last Updated**: November 26, 2025 -**Version**: 2.0 - Protocol Reference + Code Patterns (Centralized) +**Last Updated**: December 4, 2025 +**Version**: 2.1 - Added Mandatory Documentation Requirements You are helping the user create a complete Atomizer optimization study from a natural language description. @@ -9,6 +9,39 @@ You are helping the user create a complete Atomizer optimization study from a na --- +## MANDATORY DOCUMENTATION CHECKLIST + +**EVERY study MUST have these files. A study is NOT complete without them:** + +| File | Purpose | When Created | +|------|---------|--------------| +| `README.md` | **Engineering Blueprint** - Full mathematical formulation, design variables, objectives, algorithm config | At study creation | +| `STUDY_REPORT.md` | **Results Tracking** - Progress, best designs, surrogate accuracy, recommendations | At study creation (template) | + +**README.md Requirements (11 sections)**: +1. Engineering Problem (objective, physical system) +2. Mathematical Formulation (objectives, design variables, constraints with LaTeX) +3. Optimization Algorithm (config, properties, return format) +4. Simulation Pipeline (trial execution flow diagram) +5. Result Extraction Methods (extractor details, code snippets) +6. Neural Acceleration (surrogate config, expected performance) +7. Study File Structure (directory tree) +8. Results Location (output files) +9. Quick Start (commands) +10. Configuration Reference (config.json mapping) +11. References + +**STUDY_REPORT.md Requirements**: +- Executive Summary (trial counts, best values) +- Optimization Progress (iteration history, convergence) +- Best Designs Found (FEA-validated) +- Neural Surrogate Performance (R², MAE) +- Engineering Recommendations + +**FAILURE MODE**: If you create a study without README.md and STUDY_REPORT.md, the user cannot understand what the study does, the dashboard cannot display documentation, and the study is incomplete. + +--- + ## Protocol Reference (MUST USE) This section defines ALL available components. When generating `run_optimization.py`, use ONLY these documented patterns. diff --git a/atomizer-dashboard/backend/api/routes/optimization.py b/atomizer-dashboard/backend/api/routes/optimization.py index 3a105351..69c37923 100644 --- a/atomizer-dashboard/backend/api/routes/optimization.py +++ b/atomizer-dashboard/backend/api/routes/optimization.py @@ -73,7 +73,8 @@ async def list_studies(): # Protocol 10: Read from Optuna SQLite database if study_db.exists(): try: - conn = sqlite3.connect(str(study_db)) + # Use timeout to avoid blocking on locked databases + conn = sqlite3.connect(str(study_db), timeout=2.0) cursor = conn.cursor() # Get trial count and status @@ -130,6 +131,29 @@ async def list_studies(): config.get('trials', {}).get('n_trials', 50) ) + # Get creation date from directory or config modification time + created_at = None + try: + # First try to get from database (most accurate) + if study_db.exists(): + created_at = datetime.fromtimestamp(study_db.stat().st_mtime).isoformat() + elif config_file.exists(): + created_at = datetime.fromtimestamp(config_file.stat().st_mtime).isoformat() + else: + created_at = datetime.fromtimestamp(study_dir.stat().st_ctime).isoformat() + except: + created_at = None + + # Get last modified time + last_modified = None + try: + if study_db.exists(): + last_modified = datetime.fromtimestamp(study_db.stat().st_mtime).isoformat() + elif history_file.exists(): + last_modified = datetime.fromtimestamp(history_file.stat().st_mtime).isoformat() + except: + last_modified = None + studies.append({ "id": study_dir.name, "name": study_dir.name.replace("_", " ").title(), @@ -140,7 +164,9 @@ async def list_studies(): }, "best_value": best_value, "target": config.get('target', {}).get('value'), - "path": str(study_dir) + "path": str(study_dir), + "created_at": created_at, + "last_modified": last_modified }) return {"studies": studies} diff --git a/atomizer-dashboard/backend/api/routes/terminal.py b/atomizer-dashboard/backend/api/routes/terminal.py index b9b67e4d..4ce617fb 100644 --- a/atomizer-dashboard/backend/api/routes/terminal.py +++ b/atomizer-dashboard/backend/api/routes/terminal.py @@ -2,6 +2,7 @@ Terminal WebSocket for Claude Code CLI Provides a PTY-based terminal that runs Claude Code in the dashboard. +Uses pywinpty on Windows for proper interactive terminal support. """ from fastapi import APIRouter, WebSocket, WebSocketDisconnect @@ -18,6 +19,13 @@ router = APIRouter() # Store active terminal sessions _terminal_sessions: dict = {} +# Check if winpty is available (for Windows) +try: + from winpty import PtyProcess + HAS_WINPTY = True +except ImportError: + HAS_WINPTY = False + class TerminalSession: """Manages a Claude Code terminal session.""" @@ -25,10 +33,11 @@ class TerminalSession: def __init__(self, session_id: str, working_dir: str): self.session_id = session_id self.working_dir = working_dir - self.process: Optional[subprocess.Popen] = None + self.process = None self.websocket: Optional[WebSocket] = None self._read_task: Optional[asyncio.Task] = None self._running = False + self._use_winpty = sys.platform == "win32" and HAS_WINPTY async def start(self, websocket: WebSocket): """Start the Claude Code process.""" @@ -36,18 +45,34 @@ class TerminalSession: self._running = True # Determine the claude command - # On Windows, claude is typically installed via npm and available in PATH claude_cmd = "claude" - # Check if we're on Windows - is_windows = sys.platform == "win32" - try: - if is_windows: - # On Windows, use subprocess with pipes - # We need to use cmd.exe to get proper terminal behavior + if self._use_winpty: + # Use winpty for proper PTY on Windows + # Spawn claude directly - winpty handles the interactive terminal + import shutil + claude_path = shutil.which("claude") or "claude" + + # Ensure HOME and USERPROFILE are properly set for Claude CLI auth + env = {**os.environ} + env["FORCE_COLOR"] = "1" + env["TERM"] = "xterm-256color" + # Claude CLI looks for credentials in HOME/.claude or USERPROFILE/.claude + # Ensure these are set correctly + if "USERPROFILE" in env and "HOME" not in env: + env["HOME"] = env["USERPROFILE"] + + self.process = PtyProcess.spawn( + claude_path, + cwd=self.working_dir, + env=env + ) + elif sys.platform == "win32": + # Fallback: Windows without winpty - use subprocess + # Run claude with --dangerously-skip-permissions for non-interactive mode self.process = subprocess.Popen( - ["cmd.exe", "/c", claude_cmd], + ["cmd.exe", "/k", claude_cmd], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, @@ -57,7 +82,7 @@ class TerminalSession: env={**os.environ, "FORCE_COLOR": "1", "TERM": "xterm-256color"} ) else: - # On Unix, we can use pty + # On Unix, use pty import pty master_fd, slave_fd = pty.openpty() self.process = subprocess.Popen( @@ -94,34 +119,71 @@ class TerminalSession: async def _read_output(self): """Read output from the process and send to WebSocket.""" - is_windows = sys.platform == "win32" - try: - while self._running and self.process and self.process.poll() is None: - if is_windows: - # Read from stdout pipe - if self.process.stdout: - # Use asyncio to read without blocking + while self._running: + if self._use_winpty: + # Read from winpty + if self.process and self.process.isalive(): loop = asyncio.get_event_loop() try: data = await loop.run_in_executor( None, - lambda: self.process.stdout.read(1024) + lambda: self.process.read(4096) ) if data: await self.websocket.send_json({ "type": "output", - "data": data.decode("utf-8", errors="replace") + "data": data }) except Exception: break + else: + break + elif sys.platform == "win32": + # Windows subprocess pipe mode + if self.process and self.process.poll() is None: + if self.process.stdout: + loop = asyncio.get_event_loop() + try: + # Use non-blocking read with a timeout + import msvcrt + import ctypes + + # Read available data + data = await loop.run_in_executor( + None, + lambda: self.process.stdout.read(1) + ) + if data: + # Read more if available + more_data = b"" + try: + # Try to read more without blocking + while True: + extra = self.process.stdout.read(1) + if extra: + more_data += extra + else: + break + except: + pass + + full_data = data + more_data + await self.websocket.send_json({ + "type": "output", + "data": full_data.decode("utf-8", errors="replace") + }) + except Exception: + break + else: + break else: - # Read from PTY master + # Unix PTY loop = asyncio.get_event_loop() try: data = await loop.run_in_executor( None, - lambda: os.read(self._master_fd, 1024) + lambda: os.read(self._master_fd, 4096) ) if data: await self.websocket.send_json({ @@ -135,7 +197,12 @@ class TerminalSession: # Process ended if self.websocket: - exit_code = self.process.poll() if self.process else -1 + exit_code = -1 + if self._use_winpty: + exit_code = self.process.exitstatus if self.process else -1 + elif self.process: + exit_code = self.process.poll() if self.process.poll() is not None else -1 + await self.websocket.send_json({ "type": "exit", "code": exit_code @@ -156,10 +223,10 @@ class TerminalSession: if not self.process or not self._running: return - is_windows = sys.platform == "win32" - try: - if is_windows: + if self._use_winpty: + self.process.write(data) + elif sys.platform == "win32": if self.process.stdin: self.process.stdin.write(data.encode()) self.process.stdin.flush() @@ -173,8 +240,13 @@ class TerminalSession: }) async def resize(self, cols: int, rows: int): - """Resize the terminal (Unix only).""" - if sys.platform != "win32" and hasattr(self, '_master_fd'): + """Resize the terminal.""" + if self._use_winpty and self.process: + try: + self.process.setwinsize(rows, cols) + except: + pass + elif sys.platform != "win32" and hasattr(self, '_master_fd'): import struct import fcntl import termios @@ -194,14 +266,17 @@ class TerminalSession: if self.process: try: - if sys.platform == "win32": + if self._use_winpty: + self.process.terminate() + elif sys.platform == "win32": self.process.terminate() else: os.kill(self.process.pid, signal.SIGTERM) - self.process.wait(timeout=2) + self.process.wait(timeout=2) except: try: - self.process.kill() + if hasattr(self.process, 'kill'): + self.process.kill() except: pass @@ -213,16 +288,18 @@ class TerminalSession: @router.websocket("/claude") -async def claude_terminal(websocket: WebSocket, working_dir: str = None): +async def claude_terminal(websocket: WebSocket, working_dir: str = None, study_id: str = None): """ WebSocket endpoint for Claude Code terminal. Query params: working_dir: Directory to start Claude Code in (defaults to Atomizer root) + study_id: Optional study ID to set context for Claude Client -> Server messages: {"type": "input", "data": "user input text"} {"type": "resize", "cols": 80, "rows": 24} + {"type": "stop"} Server -> Client messages: {"type": "started", "message": "..."} @@ -247,6 +324,11 @@ async def claude_terminal(websocket: WebSocket, working_dir: str = None): # Start Claude Code await session.start(websocket) + # Note: Claude is started in Atomizer root directory so it has access to: + # - CLAUDE.md (system instructions) + # - .claude/skills/ (skill definitions) + # The study_id is available for the user to reference in their prompts + # Handle incoming messages while session._running: try: @@ -285,5 +367,6 @@ async def terminal_status(): return { "available": claude_path is not None, "path": claude_path, + "winpty_available": HAS_WINPTY, "message": "Claude Code CLI is available" if claude_path else "Claude Code CLI not found. Install with: npm install -g @anthropic-ai/claude-code" } diff --git a/atomizer-dashboard/frontend/src/components/ClaudeTerminal.tsx b/atomizer-dashboard/frontend/src/components/ClaudeTerminal.tsx index 4a741d93..09219a4d 100644 --- a/atomizer-dashboard/frontend/src/components/ClaudeTerminal.tsx +++ b/atomizer-dashboard/frontend/src/components/ClaudeTerminal.tsx @@ -149,21 +149,25 @@ export const ClaudeTerminal: React.FC = ({ 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`; - } + // Always use Atomizer root as working directory so Claude has access to: + // - CLAUDE.md (system instructions) + // - .claude/skills/ (skill definitions) + // Pass study_id as parameter so we can inform Claude about the context + const workingDir = 'C:/Users/Antoine/Atomizer'; + const studyParam = selectedStudy?.id ? `&study_id=${selectedStudy.id}` : ''; const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - const ws = new WebSocket(`${protocol}//${window.location.host}/api/terminal/claude${workingDir}`); + const ws = new WebSocket(`${protocol}//${window.location.host}/api/terminal/claude?working_dir=${workingDir}${studyParam}`); ws.onopen = () => { setIsConnected(true); setIsConnecting(false); xtermRef.current?.clear(); xtermRef.current?.writeln('\x1b[1;32mConnected to Claude Code\x1b[0m'); + if (selectedStudy?.id) { + xtermRef.current?.writeln(`\x1b[90mStudy context: \x1b[1;33m${selectedStudy.id}\x1b[0m`); + xtermRef.current?.writeln('\x1b[90mTip: Tell Claude about your study, e.g. "Help me with study ' + selectedStudy.id + '"\x1b[0m'); + } xtermRef.current?.writeln(''); // Send initial resize diff --git a/atomizer-dashboard/frontend/src/components/StudyReportViewer.tsx b/atomizer-dashboard/frontend/src/components/StudyReportViewer.tsx index 2350a1d7..dfd3a5ec 100644 --- a/atomizer-dashboard/frontend/src/components/StudyReportViewer.tsx +++ b/atomizer-dashboard/frontend/src/components/StudyReportViewer.tsx @@ -65,7 +65,7 @@ export function StudyReportViewer({ studyId, studyPath }: StudyReportViewerProps return (
-
+
{/* Header */}
@@ -127,8 +127,8 @@ export function StudyReportViewer({ studyId, studyPath }: StudyReportViewerProps {markdown && !loading && (
( diff --git a/atomizer-dashboard/frontend/src/index.css b/atomizer-dashboard/frontend/src/index.css index 78c5161d..77aaeb43 100644 --- a/atomizer-dashboard/frontend/src/index.css +++ b/atomizer-dashboard/frontend/src/index.css @@ -69,3 +69,39 @@ ::-webkit-scrollbar-thumb:hover { @apply bg-dark-400; } + +/* KaTeX Math Rendering */ +.katex { + font-size: 1.1em !important; + color: inherit !important; +} + +.katex-display { + margin: 1em 0 !important; + overflow-x: auto; + overflow-y: hidden; +} + +.katex-display > .katex { + color: #e2e8f0 !important; +} + +/* Markdown body styles */ +.markdown-body { + color: #e2e8f0; + line-height: 1.7; +} + +.markdown-body .katex-display { + background: rgba(30, 41, 59, 0.5); + padding: 1rem; + border-radius: 0.5rem; + border: 1px solid #334155; +} + +/* Code blocks in markdown should have proper width */ +.markdown-body pre { + max-width: 100%; + overflow-x: auto; + white-space: pre; +} diff --git a/atomizer-dashboard/frontend/src/pages/Dashboard.tsx b/atomizer-dashboard/frontend/src/pages/Dashboard.tsx index f5411602..d386e457 100644 --- a/atomizer-dashboard/frontend/src/pages/Dashboard.tsx +++ b/atomizer-dashboard/frontend/src/pages/Dashboard.tsx @@ -54,6 +54,8 @@ export default function Dashboard() { const [alertIdCounter, setAlertIdCounter] = useState(0); const [expandedTrials, setExpandedTrials] = useState>(new Set()); const [sortBy, setSortBy] = useState<'performance' | 'chronological'>('performance'); + const [trialsPage, setTrialsPage] = useState(0); + const trialsPerPage = 50; // Limit trials per page for performance // Parameter Space axis selection const [paramXIndex, setParamXIndex] = useState(0); @@ -99,6 +101,9 @@ export default function Dashboard() { }); // Load initial trial history when study changes + // PERFORMANCE: Use limit to avoid loading thousands of trials at once + const MAX_TRIALS_LOAD = 300; + useEffect(() => { if (selectedStudyId) { setAllTrials([]); @@ -106,74 +111,63 @@ export default function Dashboard() { setPrunedCount(0); setExpandedTrials(new Set()); - apiClient.getStudyHistory(selectedStudyId) + // Single history fetch with limit - used for both trial list and charts + // This replaces the duplicate fetch calls + fetch(`/api/optimization/studies/${selectedStudyId}/history?limit=${MAX_TRIALS_LOAD}`) + .then(res => res.json()) .then(data => { - const validTrials = data.trials.filter(t => t.objective !== null && t.objective !== undefined); + // Set trials for the trial list + const validTrials = data.trials.filter((t: any) => t.objective !== null && t.objective !== undefined); setAllTrials(validTrials); if (validTrials.length > 0) { - const minObj = Math.min(...validTrials.map(t => t.objective)); + const minObj = Math.min(...validTrials.map((t: any) => t.objective)); setBestValue(minObj); } - }) - .catch(console.error); - apiClient.getStudyPruning(selectedStudyId) - .then(data => { - // Use count if available (new API), fallback to array length (legacy) - setPrunedCount(data.count ?? data.pruned_trials?.length ?? 0); - }) - .catch(console.error); - - // Protocol 13: Fetch metadata - fetch(`/api/optimization/studies/${selectedStudyId}/metadata`) - .then(res => res.json()) - .then(data => { - setStudyMetadata(data); - }) - .catch(err => console.error('Failed to load metadata:', err)); - - // Protocol 13: Fetch Pareto front (raw format for Protocol 13 components) - fetch(`/api/optimization/studies/${selectedStudyId}/pareto-front`) - .then(res => res.json()) - .then(paretoData => { - console.log('[Dashboard] Pareto front data:', paretoData); - if (paretoData.is_multi_objective && paretoData.pareto_front) { - console.log('[Dashboard] Setting Pareto front with', paretoData.pareto_front.length, 'trials'); - setParetoFront(paretoData.pareto_front); - } else { - console.log('[Dashboard] No Pareto front or not multi-objective'); - setParetoFront([]); - } - }) - .catch(err => console.error('Failed to load Pareto front:', err)); - - // Fetch ALL trials (not just Pareto) for parallel coordinates and charts - fetch(`/api/optimization/studies/${selectedStudyId}/history`) - .then(res => res.json()) - .then(data => { - // Transform to match the format expected by charts - // API returns 'objectives' (array) for multi-objective, 'objective' (number) for single + // Transform for charts (parallel coordinates, etc.) const trialsData = data.trials.map((t: any) => { - // Build values array: use objectives if available, otherwise wrap single objective let values: number[] = []; if (t.objectives && Array.isArray(t.objectives)) { values = t.objectives; } else if (t.objective !== null && t.objective !== undefined) { values = [t.objective]; } - return { trial_number: t.trial_number, values, params: t.design_variables || {}, user_attrs: t.user_attrs || {}, constraint_satisfied: t.constraint_satisfied !== false, - source: t.source || t.user_attrs?.source || 'FEA' // FEA vs NN differentiation + source: t.source || t.user_attrs?.source || 'FEA' }; }); setAllTrialsRaw(trialsData); }) - .catch(err => console.error('Failed to load all trials:', err)); + .catch(console.error); + + apiClient.getStudyPruning(selectedStudyId) + .then(data => { + setPrunedCount(data.count ?? data.pruned_trials?.length ?? 0); + }) + .catch(console.error); + + // Fetch metadata (small payload) + fetch(`/api/optimization/studies/${selectedStudyId}/metadata`) + .then(res => res.json()) + .then(data => setStudyMetadata(data)) + .catch(err => console.error('Failed to load metadata:', err)); + + // Fetch Pareto front (usually small) + fetch(`/api/optimization/studies/${selectedStudyId}/pareto-front`) + .then(res => res.json()) + .then(paretoData => { + if (paretoData.is_multi_objective && paretoData.pareto_front) { + setParetoFront(paretoData.pareto_front); + } else { + setParetoFront([]); + } + }) + .catch(err => console.error('Failed to load Pareto front:', err)); } }, [selectedStudyId]); @@ -194,41 +188,77 @@ export default function Dashboard() { setDisplayedTrials(sorted); }, [allTrials, sortBy]); - // Auto-refresh polling (every 3 seconds) for trial history + // Auto-refresh polling for trial history + // PERFORMANCE: Use limit and longer interval for large studies useEffect(() => { if (!selectedStudyId) return; const refreshInterval = setInterval(() => { - apiClient.getStudyHistory(selectedStudyId) + // Only fetch latest trials, not the entire history + fetch(`/api/optimization/studies/${selectedStudyId}/history?limit=${MAX_TRIALS_LOAD}`) + .then(res => res.json()) .then(data => { - const validTrials = data.trials.filter(t => t.objective !== null && t.objective !== undefined); + const validTrials = data.trials.filter((t: any) => t.objective !== null && t.objective !== undefined); setAllTrials(validTrials); if (validTrials.length > 0) { - const minObj = Math.min(...validTrials.map(t => t.objective)); + const minObj = Math.min(...validTrials.map((t: any) => t.objective)); setBestValue(minObj); } + // Also update chart data + const trialsData = data.trials.map((t: any) => { + let values: number[] = []; + if (t.objectives && Array.isArray(t.objectives)) { + values = t.objectives; + } else if (t.objective !== null && t.objective !== undefined) { + values = [t.objective]; + } + return { + trial_number: t.trial_number, + values, + params: t.design_variables || {}, + user_attrs: t.user_attrs || {}, + constraint_satisfied: t.constraint_satisfied !== false, + source: t.source || t.user_attrs?.source || 'FEA' + }; + }); + setAllTrialsRaw(trialsData); }) .catch(err => console.error('Auto-refresh failed:', err)); - }, 3000); // Poll every 3 seconds + }, 15000); // Poll every 15 seconds for performance return () => clearInterval(refreshInterval); }, [selectedStudyId]); - // Prepare chart data with proper null/undefined handling - const convergenceData: ConvergenceDataPoint[] = allTrials - .filter(t => t.objective !== null && t.objective !== undefined) - .sort((a, b) => a.trial_number - b.trial_number) - .map((trial, idx, arr) => { - const previousTrials = arr.slice(0, idx + 1); - const validObjectives = previousTrials.map(t => t.objective).filter(o => o !== null && o !== undefined); - return { - trial_number: trial.trial_number, - objective: trial.objective, - best_so_far: validObjectives.length > 0 ? Math.min(...validObjectives) : trial.objective, - }; - }); + // Sample data for charts when there are too many trials (performance optimization) + const MAX_CHART_POINTS = 200; // Reduced for better performance + const sampleData = (data: T[], maxPoints: number): T[] => { + if (data.length <= maxPoints) return data; + const step = Math.ceil(data.length / maxPoints); + return data.filter((_, i) => i % step === 0 || i === data.length - 1); + }; - const parameterSpaceData: ParameterSpaceDataPoint[] = allTrials + // Prepare chart data with proper null/undefined handling + const allValidTrials = allTrials + .filter(t => t.objective !== null && t.objective !== undefined) + .sort((a, b) => a.trial_number - b.trial_number); + + // Calculate best_so_far for each trial + let runningBest = Infinity; + const convergenceDataFull: ConvergenceDataPoint[] = allValidTrials.map(trial => { + if (trial.objective < runningBest) { + runningBest = trial.objective; + } + return { + trial_number: trial.trial_number, + objective: trial.objective, + best_so_far: runningBest, + }; + }); + + // Sample for chart rendering performance + const convergenceData = sampleData(convergenceDataFull, MAX_CHART_POINTS); + + const parameterSpaceDataFull: ParameterSpaceDataPoint[] = allTrials .filter(t => t.objective !== null && t.objective !== undefined && t.design_variables) .map(trial => { const params = Object.values(trial.design_variables); @@ -241,6 +271,9 @@ export default function Dashboard() { }; }); + // Sample for chart rendering performance + const parameterSpaceData = sampleData(parameterSpaceDataFull, MAX_CHART_POINTS); + // Calculate average objective const validObjectives = allTrials.filter(t => t.objective !== null && t.objective !== undefined).map(t => t.objective); const avgObjective = validObjectives.length > 0 @@ -384,14 +417,14 @@ export default function Dashboard() {
-
- {/* Control Panel - Left Sidebar */} -