feat: Dashboard improvements and configuration updates

Dashboard:
- Enhanced terminal components (ClaudeTerminal, GlobalClaudeTerminal)
- Improved MarkdownRenderer for better documentation display
- Updated convergence plots (ConvergencePlot, PlotlyConvergencePlot)
- Refined Home, Analysis, Dashboard, Setup, Results pages
- Added StudyContext improvements
- Updated vite.config for better dev experience

Configuration:
- Updated CLAUDE.md with latest instructions
- Enhanced launch_dashboard.py
- Updated config.py settings

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-20 13:47:05 -05:00
parent 1612991d0d
commit 7c700c4606
19 changed files with 478 additions and 173 deletions

View File

@@ -47,7 +47,65 @@
"Bash(cmd.exe /c:*)", "Bash(cmd.exe /c:*)",
"Bash(powershell.exe -Command:*)", "Bash(powershell.exe -Command:*)",
"Bash(where:*)", "Bash(where:*)",
"Bash(type %USERPROFILE%.claude*)" "Bash(type %USERPROFILE%.claude*)",
"Bash(conda create:*)",
"Bash(cmd /c \"conda create -n atomizer python=3.10 -y\")",
"Bash(cmd /c \"where conda\")",
"Bash(cmd /c \"dir /b C:\\Users\\antoi\\anaconda3\\Scripts\\conda.exe 2>nul || dir /b C:\\Users\\antoi\\miniconda3\\Scripts\\conda.exe 2>nul || dir /b C:\\ProgramData\\anaconda3\\Scripts\\conda.exe 2>nul || dir /b C:\\ProgramData\\miniconda3\\Scripts\\conda.exe 2>nul || echo NOT_FOUND\")",
"Bash(cmd /c \"if exist C:\\Users\\antoi\\anaconda3\\Scripts\\conda.exe (echo FOUND: anaconda3) else if exist C:\\Users\\antoi\\miniconda3\\Scripts\\conda.exe (echo FOUND: miniconda3) else if exist C:\\ProgramData\\anaconda3\\Scripts\\conda.exe (echo FOUND: ProgramData\\anaconda3) else (echo NOT_FOUND)\")",
"Bash(powershell:*)",
"Bash(C:Usersantoianaconda3Scriptsconda.exe create -n atomizer python=3.10 -y)",
"Bash(cmd /c \"C:\\Users\\antoi\\anaconda3\\Scripts\\conda.exe create -n atomizer python=3.10 -y\")",
"Bash(cmd /c \"set SPLM_LICENSE_SERVER=28000@dalidou;28000@100.80.199.40 && \"\"C:\\Program Files\\Siemens\\DesigncenterNX2512\\NXBIN\\run_journal.exe\"\" \"\"C:\\Users\\antoi\\Atomizer\\optimization_engine\\solve_simulation.py\"\" -args \"\"C:\\Users\\antoi\\Atomizer\\studies\\m1_mirror_adaptive_V15\\2_iterations\\iter2\\ASSY_M1_assyfem1_sim1.sim\"\" \"\"Solution 1\"\" 2>&1\")",
"Bash(cmd /c \"set SPLM_LICENSE_SERVER=28000@dalidou;28000@100.80.199.40 && \"C:Program FilesSiemensDesigncenterNX2512NXBINrun_journal.exe\" \"C:UsersantoiAtomizernx_journalsextract_part_mass_material.py\" -args \"C:UsersantoiAtomizerstudiesm1_mirror_cost_reduction1_setupmodelM1_Blank.prt\" \"C:UsersantoiAtomizerstudiesm1_mirror_cost_reduction1_setupmodel\" 2>&1\")",
"Bash(npm run dev:*)",
"Bash(cmd /c \"cd /d C:\\Users\\antoi\\Atomizer\\atomizer-dashboard\\frontend && npm run dev\")",
"Bash(cmd /c \"cd /d C:\\Users\\antoi\\Atomizer\\atomizer-dashboard\\frontend && dir package.json && npm --version\")",
"Bash(cmd /c \"set SPLM_LICENSE_SERVER=28000@dalidou;28000@100.80.199.40 && \"\"C:\\Program Files\\Siemens\\DesigncenterNX2512\\NXBIN\\run_journal.exe\"\" \"\"C:\\Users\\antoi\\Atomizer\\nx_journals\\extract_part_mass_material.py\"\" -args \"\"C:\\Users\\antoi\\Atomizer\\studies\\m1_mirror_cost_reduction\\1_setup\\model\\M1_Blank.prt\"\" \"\"C:\\Users\\antoi\\Atomizer\\studies\\m1_mirror_cost_reduction\\1_setup\\model\"\" 2>&1\")",
"Bash(cmd /c \"set SPLM_LICENSE_SERVER=28000@dalidou;28000@100.80.199.40 && \"\"C:\\Program Files\\Siemens\\DesigncenterNX2512\\NXBIN\\run_journal.exe\"\" \"\"C:\\Users\\antoi\\Atomizer\\nx_journals\\extract_expressions.py\"\" -args \"\"C:\\Users\\antoi\\Atomizer\\studies\\m1_mirror_cost_reduction\\1_setup\\model\\M1_Blank.prt\"\" \"\"C:\\Users\\antoi\\Atomizer\\studies\\m1_mirror_cost_reduction\\1_setup\\model\"\" 2>&1\")",
"Bash(cmd /c \"set SPLM_LICENSE_SERVER=28000@dalidou;28000@100.80.199.40 && \"\"C:\\Program Files\\Siemens\\DesigncenterNX2512\\NXBIN\\run_journal.exe\"\" \"\"C:\\Users\\antoi\\Atomizer\\nx_journals\\extract_expressions.py\"\" -args \"\"C:\\Users\\antoi\\Atomizer\\studies\\m1_mirror_cost_reduction\\1_setup\\model\\M1_Blank.prt\"\" \"\"C:\\Users\\antoi\\Atomizer\\studies\\m1_mirror_cost_reduction\\1_setup\\model\"\"\")",
"Bash(cmd /c:*)",
"Bash(taskkill /F /FI \"WINDOWTITLE eq *uvicorn*\")",
"Bash(python -m uvicorn:*)",
"Bash(conda run:*)",
"Bash(/c/Users/antoi/miniconda3/envs/atomizer/python.exe -m uvicorn:*)",
"Bash(/c/Users/antoi/anaconda3/envs/atomizer/python.exe -m uvicorn:*)",
"Bash(/c/Users/antoi/anaconda3/envs/atomizer/python.exe:*)",
"Bash(tasklist:*)",
"Bash(wmic process where \"ProcessId=147068\" delete)",
"Bash(cmd.exe //c \"taskkill /F /PID 147068\")",
"Bash(pip show:*)",
"Bash(python3:*)",
"Bash(python extract_all_mirror_data.py:*)",
"Bash(C:Usersantoiminiconda3envsatomizerpython.exe extract_all_mirror_data.py)",
"Bash(/c/Users/antoi/miniconda3/envs/atomizer/python.exe:*)",
"Bash(grep:*)",
"Bash(python -c:*)",
"Bash(C:Usersantoianaconda3envsatomizerpython.exe -c \"\nimport pandas as pd\ndf = pd.read_csv(r''c:\\Users\\antoi\\Atomizer\\studies\\m1_mirror_all_trials_export.csv'')\n\n# Check which columns have data\nprint(''=== Column data availability ==='')\nfor col in df.columns:\n non_null = df[col].notna().sum()\n print(f''{col}: {non_null}/{len(df)} ({100*non_null/len(df):.1f}%)'')\n\nprint(''\\n=== Studies in dataset ==='')\nprint(df[''study''].value_counts())\n\")",
"Bash(cmd /c \"C:\\Users\\antoi\\anaconda3\\envs\\atomizer\\python.exe -c \"\"import pandas as pd; df = pd.read_csv(r''c:\\Users\\antoi\\Atomizer\\studies\\m1_mirror_all_trials_export.csv''); print(''Rows:'', len(df)); print(df.columns.tolist())\"\"\")",
"Bash(robocopy:*)",
"Bash(xcopy:*)",
"Bash(ls:*)",
"Bash(dir \"c:\\Users\\antoi\\Atomizer\\studies\\*.png\")",
"Bash(powershell -Command \"Get-Process | Where-Object { $_Modules.FileName -like ''*study.db*'' } | Select-Object Id, ProcessName\")",
"Bash(powershell -Command:*)",
"Bash(C:/Users/antoi/miniconda3/envs/atomizer/python.exe -m uvicorn:*)",
"Bash(dir /s /b \"C:\\Users\\antoi\\*conda*\")",
"Bash(conda run -n atomizer python:*)",
"Bash(C:/ProgramData/anaconda3/condabin/conda.bat run -n atomizer python -c \"\nimport sqlite3\n\ndb_path = ''studies/M1_Mirror/m1_mirror_cost_reduction_V6/3_results/study.db''\nconn = sqlite3.connect(db_path)\ncursor = conn.cursor()\n\n# Get counts\ncursor.execute(''SELECT COUNT(*) FROM trials'')\ntotal = cursor.fetchone()[0]\n\ncursor.execute(\"\"SELECT COUNT(*) FROM trials WHERE state = ''COMPLETE''\"\")\ncomplete = cursor.fetchone()[0]\n\nprint(f''=== V6 Study Status ==='')\nprint(f''Total trials: {total}'')\nprint(f''Completed: {complete}'')\nprint(f''Failed/Pruned: {total - complete}'')\nprint(f''Progress: {complete}/200 ({100*complete/200:.1f}%)'')\n\n# Get objectives stats\nobjs = [''rel_filtered_rms_40_vs_20'', ''rel_filtered_rms_60_vs_20'', ''mfg_90_optician_workload'', ''mass_kg'']\nprint(f''\\n=== Objectives Stats ==='')\nfor obj in objs:\n cursor.execute(f\"\"SELECT MIN({obj}), MAX({obj}), AVG({obj}) FROM trials WHERE state = ''COMPLETE'' AND {obj} IS NOT NULL\"\")\n result = cursor.fetchone()\n if result and result[0] is not None:\n print(f''{obj}: min={result[0]:.4f}, max={result[1]:.4f}, mean={result[2]:.4f}'')\n\n# Design variables stats \ndvs = [''whiffle_min'', ''whiffle_outer_to_vertical'', ''whiffle_triangle_closeness'', ''blank_backface_angle'', ''Pocket_Radius'']\nprint(f''\\n=== Design Variables Explored ==='')\nfor dv in dvs:\n try:\n cursor.execute(f\"\"SELECT MIN({dv}), MAX({dv}), AVG({dv}) FROM trials WHERE state = ''COMPLETE''\"\")\n result = cursor.fetchone()\n if result and result[0] is not None:\n print(f''{dv}: min={result[0]:.3f}, max={result[1]:.3f}, mean={result[2]:.3f}'')\n except Exception as e:\n print(f''{dv}: error - {e}'')\n\nconn.close()\n\")",
"Bash(/c/Users/antoi/anaconda3/python.exe:*)",
"Bash(C:UsersantoiAtomizertemp_extract.bat)",
"Bash(dir /b \"C:\\Users\\antoi\\Atomizer\\knowledge_base\\lac\")",
"Bash(pip install:*)",
"Bash(dir \"C:\\Users\\antoi\\Atomizer\\studies\\M1_Mirror\\m1_mirror_cost_reduction_V7\\3_results\")",
"Bash(call \"%USERPROFILE%\\anaconda3\\Scripts\\activate.bat\" atomizer)",
"Bash(cmd /c \"cd /d c:\\Users\\antoi\\Atomizer && call %USERPROFILE%\\anaconda3\\Scripts\\activate.bat atomizer && python -c \"\"import sys; sys.path.insert(0, ''.''); from optimization_engine.extractors import ZernikeExtractor; print(''OK''); import inspect; print(inspect.signature(ZernikeExtractor.extract_relative))\"\"\")",
"Bash(cmd /c \"cd /d c:\\Users\\antoi\\Atomizer && c:\\Users\\antoi\\anaconda3\\envs\\atomizer\\python.exe -c \"\"import sys; sys.path.insert(0, ''.''); from optimization_engine.extractors import ZernikeExtractor; print(''Import OK''); import inspect; sig = inspect.signature(ZernikeExtractor.extract_relative); print(''Signature:'', sig)\"\"\")",
"Bash(c:Usersantoianaconda3envsatomizerpython.exe c:UsersantoiAtomizertoolstest_zernike_import.py)",
"Bash(dir \"C:\\Users\\antoi\\Atomizer\\studies\\M1_Mirror\\m1_mirror_cost_reduction_V7\\3_results\\best_design_archive\")",
"Bash(dir \"C:\\Users\\antoi\\Atomizer\\studies\\M1_Mirror\\m1_mirror_cost_reduction_V7\\3_results\\best_design_archive\\20251220_010128\")",
"Bash(dir /s /b \"C:\\Users\\antoi\\Atomizer\\studies\\M1_Mirror\\m1_mirror_cost_reduction_V8\")",
"Bash(c:/Users/antoi/anaconda3/envs/atomizer/python.exe:*)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []

View File

@@ -18,7 +18,7 @@ On **EVERY new Claude session**, perform these initialization steps:
1. Read `.claude/ATOMIZER_CONTEXT.md` for unified context (if not already loaded via this file) 1. Read `.claude/ATOMIZER_CONTEXT.md` for unified context (if not already loaded via this file)
2. This file (CLAUDE.md) provides system instructions 2. This file (CLAUDE.md) provides system instructions
3. Use `.claude/skills/00_BOOTSTRAP.md` for task routing 3. Use `.claude/skills/00_BOOTSTRAP.md` for task routing
4. Check `knowledge_base/lac/` for relevant prior learnings (see LAC section below) 4. **MANDATORY: Read `knowledge_base/lac/session_insights/failure.jsonl`** - Contains critical lessons from past sessions. These are hard-won insights about what NOT to do.
### Step 2: Detect Study Context ### Step 2: Detect Study Context
If working directory is inside a study (`studies/*/`): If working directory is inside a study (`studies/*/`):
@@ -27,16 +27,26 @@ If working directory is inside a study (`studies/*/`):
3. Summarize study state to user in first response 3. Summarize study state to user in first response
### Step 3: Route by User Intent ### Step 3: Route by User Intent
**CRITICAL: Actually READ the protocol file before executing the task. Don't work from memory.**
| User Keywords | Load Protocol | Subagent Type | | User Keywords | Load Protocol | Subagent Type |
|---------------|---------------|---------------| |---------------|---------------|---------------|
| "create", "new", "set up" | OP_01, SYS_12 | general-purpose | | "create", "new", "set up" | **READ** OP_01 first, then execute | general-purpose |
| "run", "start", "trials" | OP_02, SYS_15 | - (direct execution) | | "run", "start", "trials" | **READ** OP_02 first | - (direct execution) |
| "status", "progress" | OP_03 | - (DB query) | | "status", "progress" | OP_03 | - (DB query) |
| "results", "analyze", "Pareto" | OP_04 | - (analysis) | | "results", "analyze", "Pareto" | OP_04 | - (analysis) |
| "neural", "surrogate", "turbo" | SYS_14, SYS_15 | general-purpose | | "neural", "surrogate", "turbo" | SYS_14, SYS_15 | general-purpose |
| "NX", "model", "expression" | MCP siemens-docs | general-purpose | | "NX", "model", "expression" | MCP siemens-docs | general-purpose |
| "error", "fix", "debug" | OP_06 | Explore | | "error", "fix", "debug" | OP_06 | Explore |
**Protocol Loading Rule**: When a task matches a protocol (e.g., "create study" → OP_01), you MUST:
1. Read the protocol file (`docs/protocols/operations/OP_01_CREATE_STUDY.md`)
2. Extract the checklist/required outputs
3. Add ALL items to TodoWrite
4. Execute each item
5. Mark complete ONLY when all checklist items are done
### Step 4: Proactive Actions ### Step 4: Proactive Actions
- If optimization is running: Report progress automatically - If optimization is running: Report progress automatically
- If no study context: Offer to create one or list available studies - If no study context: Offer to create one or list available studies

View File

@@ -20,9 +20,43 @@ router = APIRouter()
_terminal_sessions: dict = {} _terminal_sessions: dict = {}
# Path to Atomizer root (for loading prompts) # Path to Atomizer root (for loading prompts)
ATOMIZER_ROOT = os.path.dirname(os.path.dirname(os.path.dirname( # Go up 5 levels: terminal.py -> routes -> api -> backend -> atomizer-dashboard -> Atomizer
os.path.dirname(os.path.dirname(os.path.abspath(__file__))) _file_path = os.path.abspath(__file__)
))) ATOMIZER_ROOT = os.path.normpath(os.path.dirname(os.path.dirname(os.path.dirname(
os.path.dirname(os.path.dirname(_file_path))
))))
STUDIES_DIR = os.path.join(ATOMIZER_ROOT, "studies")
# Debug: print the resolved path at module load
print(f"[Terminal] ATOMIZER_ROOT resolved to: {ATOMIZER_ROOT}")
def resolve_study_path(study_id: str) -> str:
"""Find study folder by scanning topic directories.
Returns relative path from ATOMIZER_ROOT (e.g., 'studies/M1_Mirror/m1_mirror_adaptive_V14').
"""
# First check direct path (flat structure)
direct_path = os.path.join(STUDIES_DIR, study_id)
if os.path.isdir(direct_path):
setup_dir = os.path.join(direct_path, "1_setup")
config_file = os.path.join(direct_path, "optimization_config.json")
if os.path.exists(setup_dir) or os.path.exists(config_file):
return f"studies/{study_id}"
# Scan topic folders for nested structure
if os.path.isdir(STUDIES_DIR):
for topic_name in os.listdir(STUDIES_DIR):
topic_path = os.path.join(STUDIES_DIR, topic_name)
if os.path.isdir(topic_path) and not topic_name.startswith('.'):
study_path = os.path.join(topic_path, study_id)
if os.path.isdir(study_path):
setup_dir = os.path.join(study_path, "1_setup")
config_file = os.path.join(study_path, "optimization_config.json")
if os.path.exists(setup_dir) or os.path.exists(config_file):
return f"studies/{topic_name}/{study_id}"
# Fallback to flat path
return f"studies/{study_id}"
def get_session_prompt(study_name: str = None) -> str: def get_session_prompt(study_name: str = None) -> str:
@@ -54,19 +88,21 @@ def get_session_prompt(study_name: str = None) -> str:
] ]
if study_name: if study_name:
# Resolve actual study path (handles nested folder structure)
study_path = resolve_study_path(study_name)
prompt_lines.extend([ prompt_lines.extend([
f"## Current Study: `{study_name}`", f"## Current Study: `{study_name}`",
"", "",
f"**Directory**: `studies/{study_name}/`", f"**Directory**: `{study_path}/`",
"", "",
"Key files:", "Key files:",
f"- `studies/{study_name}/1_setup/optimization_config.json` - Configuration", f"- `{study_path}/1_setup/optimization_config.json` - Configuration",
f"- `studies/{study_name}/2_results/study.db` - Optuna database", f"- `{study_path}/3_results/study.db` - Optuna database",
f"- `studies/{study_name}/README.md` - Study documentation", f"- `{study_path}/README.md` - Study documentation",
"", "",
"Quick status check:", "Quick status check:",
"```bash", "```bash",
f"python -c \"import optuna; s=optuna.load_study('{study_name}', 'sqlite:///studies/{study_name}/2_results/study.db'); print(f'Trials: {{len(s.trials)}}, Best: {{s.best_value}}')\"", f"python -c \"import optuna; s=optuna.load_study('{study_name}', 'sqlite:///{study_path}/3_results/study.db'); print(f'Trials: {{len(s.trials)}}, Best: {{s.best_value}}')\"",
"```", "```",
"", "",
]) ])
@@ -99,8 +135,10 @@ def get_session_prompt(study_name: str = None) -> str:
try: try:
from winpty import PtyProcess from winpty import PtyProcess
HAS_WINPTY = True HAS_WINPTY = True
except ImportError: print("[Terminal] winpty is available")
except ImportError as e:
HAS_WINPTY = False HAS_WINPTY = False
print(f"[Terminal] winpty not available: {e}")
class TerminalSession: class TerminalSession:
@@ -120,6 +158,15 @@ class TerminalSession:
self.websocket = websocket self.websocket = websocket
self._running = True self._running = True
# Validate working directory exists
if not os.path.isdir(self.working_dir):
await self.websocket.send_json({
"type": "error",
"message": f"Working directory does not exist: {self.working_dir}"
})
self._running = False
return
try: try:
if self._use_winpty: if self._use_winpty:
# Use winpty for proper PTY on Windows # Use winpty for proper PTY on Windows
@@ -143,7 +190,8 @@ class TerminalSession:
) )
elif sys.platform == "win32": elif sys.platform == "win32":
# Fallback: Windows without winpty - use subprocess # Fallback: Windows without winpty - use subprocess
# Run claude with --dangerously-skip-permissions for non-interactive mode import shutil
claude_cmd = shutil.which("claude") or "claude"
self.process = subprocess.Popen( self.process = subprocess.Popen(
["cmd.exe", "/k", claude_cmd], ["cmd.exe", "/k", claude_cmd],
stdin=subprocess.PIPE, stdin=subprocess.PIPE,
@@ -157,6 +205,8 @@ class TerminalSession:
else: else:
# On Unix, use pty # On Unix, use pty
import pty import pty
import shutil
claude_cmd = shutil.which("claude") or "claude"
master_fd, slave_fd = pty.openpty() master_fd, slave_fd = pty.openpty()
self.process = subprocess.Popen( self.process = subprocess.Popen(
[claude_cmd], [claude_cmd],
@@ -467,6 +517,9 @@ async def get_context(study_id: str = None):
""" """
prompt = get_session_prompt(study_id) prompt = get_session_prompt(study_id)
# Resolve study path for nested folder structure
study_path = resolve_study_path(study_id) if study_id else None
return { return {
"study_id": study_id, "study_id": study_id,
"prompt": prompt, "prompt": prompt,
@@ -476,8 +529,8 @@ async def get_context(study_id: str = None):
".claude/skills/02_CONTEXT_LOADER.md", ".claude/skills/02_CONTEXT_LOADER.md",
], ],
"study_files": [ "study_files": [
f"studies/{study_id}/1_setup/optimization_config.json", f"{study_path}/1_setup/optimization_config.json",
f"studies/{study_id}/2_results/study.db", f"{study_path}/3_results/study.db",
f"studies/{study_id}/README.md", f"{study_path}/README.md",
] if study_id else [] ] if study_path else []
} }

View File

@@ -161,15 +161,12 @@ export const ClaudeTerminal: React.FC<ClaudeTerminalProps> = ({
setIsConnecting(true); setIsConnecting(true);
setError(null); setError(null);
// Always use Atomizer root as working directory so Claude has access to: // Let backend determine the working directory (ATOMIZER_ROOT)
// - CLAUDE.md (system instructions)
// - .claude/skills/ (skill definitions)
// Pass study_id as parameter so we can inform Claude about the context // 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 studyParam = selectedStudy?.id ? `&study_id=${selectedStudy.id}` : '';
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const ws = new WebSocket(`${protocol}//${window.location.host}/api/terminal/claude?working_dir=${workingDir}${studyParam}`); const ws = new WebSocket(`${protocol}//${window.location.host}/api/terminal/claude${studyParam}`);
ws.onopen = () => { ws.onopen = () => {
setIsConnected(true); setIsConnected(true);

View File

@@ -20,8 +20,13 @@ interface Trial {
trial_number: number; trial_number: number;
values: number[]; values: number[];
state?: string; state?: string;
constraint_satisfied?: boolean;
user_attrs?: Record<string, any>;
} }
// Penalty threshold - objectives above this are considered failed/penalty trials
const PENALTY_THRESHOLD = 100000;
interface ConvergencePlotProps { interface ConvergencePlotProps {
trials: Trial[]; trials: Trial[];
objectiveIndex?: number; objectiveIndex?: number;
@@ -38,9 +43,22 @@ export function ConvergencePlot({
const convergenceData = useMemo(() => { const convergenceData = useMemo(() => {
if (!trials || trials.length === 0) return []; if (!trials || trials.length === 0) return [];
// Sort by trial number // Sort by trial number, filtering out failed/penalty trials
const sortedTrials = [...trials] const sortedTrials = [...trials]
.filter(t => t.values && t.values.length > objectiveIndex && t.state !== 'FAIL') .filter(t => {
// Must have valid values
if (!t.values || t.values.length <= objectiveIndex) return false;
// Filter out failed state
if (t.state === 'FAIL') return false;
// Filter out penalty values (e.g., 1000000 = solver failure)
const val = t.values[objectiveIndex];
if (val >= PENALTY_THRESHOLD) return false;
// Filter out constraint violations
if (t.constraint_satisfied === false) return false;
// Filter out pruned trials
if (t.user_attrs?.pruned === true || t.user_attrs?.fail_reason) return false;
return true;
})
.sort((a, b) => a.trial_number - b.trial_number); .sort((a, b) => a.trial_number - b.trial_number);
if (sortedTrials.length === 0) return []; if (sortedTrials.length === 0) return [];

View File

@@ -33,12 +33,14 @@ export const GlobalClaudeTerminal: React.FC = () => {
); );
} }
// Terminal panel // Terminal panel - responsive sizing
// On mobile portrait: full width with small margins
// On tablet/desktop: fixed size panel
return ( return (
<div className={`fixed z-50 transition-all duration-200 ${ <div className={`fixed z-50 transition-all duration-200 ${
isExpanded isExpanded
? 'inset-4' ? 'inset-2 sm:inset-4'
: 'bottom-6 right-6 w-[650px] h-[500px]' : 'bottom-2 right-2 left-2 h-[400px] sm:bottom-6 sm:right-6 sm:left-auto sm:w-[650px] sm:h-[500px]'
}`}> }`}>
<ClaudeTerminal <ClaudeTerminal
isExpanded={isExpanded} isExpanded={isExpanded}

View File

@@ -10,13 +10,34 @@ import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
interface MarkdownRendererProps { interface MarkdownRendererProps {
content: string; content: string;
className?: string; className?: string;
studyId?: string; // Optional study ID for resolving relative image paths
} }
/** /**
* Shared markdown renderer with syntax highlighting, GFM, and LaTeX support. * Shared markdown renderer with syntax highlighting, GFM, and LaTeX support.
* Used by both the Home page (README display) and Results page (reports). * Used by both the Home page (README display) and Results page (reports).
*/ */
export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content, className = '' }) => { export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content, className = '', studyId }) => {
// Helper to resolve image URLs - converts relative paths to API endpoints
const resolveImageSrc = (src: string | undefined): string => {
if (!src) return '';
// If it's already an absolute URL or data URL, return as-is
if (src.startsWith('http://') || src.startsWith('https://') || src.startsWith('data:')) {
return src;
}
// If we have a studyId, route through the API
if (studyId) {
// Remove leading ./ or / from the path
const cleanPath = src.replace(/^\.?\//, '');
return `/api/optimization/studies/${studyId}/image/${cleanPath}`;
}
// Fallback: return original src
return src;
};
return ( return (
<article className={`markdown-body max-w-none ${className}`}> <article className={`markdown-body max-w-none ${className}`}>
<ReactMarkdown <ReactMarkdown
@@ -165,12 +186,17 @@ export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content, cla
hr: () => ( hr: () => (
<hr className="my-8 border-dark-600" /> <hr className="my-8 border-dark-600" />
), ),
// Images // Images - resolve relative paths through API
img: ({ src, alt }) => ( img: ({ src, alt }) => (
<img <img
src={src} src={resolveImageSrc(src)}
alt={alt} alt={alt || ''}
className="my-4 rounded-lg max-w-full h-auto border border-dark-600" className="my-4 rounded-lg max-w-full h-auto border border-dark-600"
loading="lazy"
onError={(e) => {
// Hide broken images
(e.target as HTMLImageElement).style.display = 'none';
}}
/> />
), ),
}} }}

View File

@@ -5,8 +5,8 @@ export const MainLayout = () => {
return ( return (
<div className="min-h-screen bg-dark-900 text-dark-50 font-sans"> <div className="min-h-screen bg-dark-900 text-dark-50 font-sans">
<Sidebar /> <Sidebar />
<main className="ml-64 min-h-screen"> <main className="ml-64 min-h-screen p-6">
<div className="max-w-7xl mx-auto p-8"> <div className="max-w-6xl">
<Outlet /> <Outlet />
</div> </div>
</main> </main>

View File

@@ -19,8 +19,12 @@ interface Trial {
params: Record<string, number>; params: Record<string, number>;
user_attrs?: Record<string, any>; user_attrs?: Record<string, any>;
source?: 'FEA' | 'NN' | 'V10_FEA'; source?: 'FEA' | 'NN' | 'V10_FEA';
constraint_satisfied?: boolean;
} }
// Penalty threshold - objectives above this are considered failed/penalty trials
const PENALTY_THRESHOLD = 100000;
interface PlotlyConvergencePlotProps { interface PlotlyConvergencePlotProps {
trials: Trial[]; trials: Trial[];
objectiveIndex?: number; objectiveIndex?: number;
@@ -58,6 +62,15 @@ export function PlotlyConvergencePlot({
const val = t.values?.[objectiveIndex] ?? t.user_attrs?.[objectiveName] ?? null; const val = t.values?.[objectiveIndex] ?? t.user_attrs?.[objectiveName] ?? null;
if (val === null || !isFinite(val)) return; if (val === null || !isFinite(val)) return;
// Filter out failed/penalty trials:
// 1. Objective above penalty threshold (e.g., 1000000 = solver failure)
// 2. constraint_satisfied explicitly false
// 3. user_attrs indicates pruned/failed
const isPenalty = val >= PENALTY_THRESHOLD;
const isFailed = t.constraint_satisfied === false;
const isPruned = t.user_attrs?.pruned === true || t.user_attrs?.fail_reason;
if (isPenalty || isFailed || isPruned) return;
const source = t.source || t.user_attrs?.source || 'FEA'; const source = t.source || t.user_attrs?.source || 'FEA';
const hoverText = `Trial #${t.trial_number}<br>${objectiveName}: ${val.toFixed(4)}<br>Source: ${source}`; const hoverText = `Trial #${t.trial_number}<br>${objectiveName}: ${val.toFixed(4)}<br>Source: ${source}`;

View File

@@ -58,7 +58,9 @@ export const StudyProvider: React.FC<{ children: ReactNode }> = ({ children }) =
useEffect(() => { useEffect(() => {
const init = async () => { const init = async () => {
try { try {
console.log('[StudyContext] Fetching studies...');
const response = await apiClient.getStudies(); const response = await apiClient.getStudies();
console.log('[StudyContext] Got studies:', response.studies.length, response.studies);
setStudies(response.studies); setStudies(response.studies);
// Restore last selected study from localStorage // Restore last selected study from localStorage
@@ -70,8 +72,9 @@ export const StudyProvider: React.FC<{ children: ReactNode }> = ({ children }) =
} }
} }
} catch (error) { } catch (error) {
console.error('Failed to initialize studies:', error); console.error('[StudyContext] Failed to initialize studies:', error);
} finally { } finally {
console.log('[StudyContext] Initialization complete, isLoading=false');
setIsLoading(false); setIsLoading(false);
setIsInitialized(true); // Mark as initialized AFTER localStorage restoration setIsInitialized(true); // Mark as initialized AFTER localStorage restoration
} }

View File

@@ -227,7 +227,7 @@ export default function Analysis() {
const isMultiObjective = (metadata?.objectives?.length || 0) > 1; const isMultiObjective = (metadata?.objectives?.length || 0) > 1;
return ( return (
<div className="w-full max-w-[2400px] mx-auto px-4"> <div className="w-full">
{/* Header */} {/* Header */}
<header className="mb-6 flex items-center justify-between border-b border-dark-600 pb-4"> <header className="mb-6 flex items-center justify-between border-b border-dark-600 pb-4">
<div> <div>

View File

@@ -375,7 +375,7 @@ export default function Dashboard() {
}; };
return ( return (
<div className="w-full max-w-[2400px] mx-auto px-4"> <div className="w-full">
{/* Alerts */} {/* Alerts */}
<div className="fixed top-4 right-4 z-50 space-y-2"> <div className="fixed top-4 right-4 z-50 space-y-2">
{alerts.map(alert => ( {alerts.map(alert => (
@@ -436,13 +436,21 @@ export default function Dashboard() {
<StudyReportViewer studyId={selectedStudyId} /> <StudyReportViewer studyId={selectedStudyId} />
)} )}
<button <button
onClick={() => { onClick={async () => {
// Open Optuna dashboard on port 8081 if (!selectedStudyId) return;
// Note: The dashboard needs to be started separately with the correct study database try {
window.open('http://localhost:8081', '_blank'); // Launch Optuna dashboard via API, then open the returned URL
const result = await apiClient.launchOptunaDashboard(selectedStudyId);
window.open(result.url || 'http://localhost:8081', '_blank');
} catch (err) {
// If launch fails (maybe already running), try opening directly
console.warn('Failed to launch dashboard:', err);
window.open('http://localhost:8081', '_blank');
}
}} }}
className="btn-secondary" className="btn-secondary"
title="Open Optuna Dashboard (runs on port 8081)" title="Launch Optuna Dashboard for this study"
disabled={!selectedStudyId}
> >
Optuna Dashboard Optuna Dashboard
</button> </button>

View File

@@ -10,11 +10,16 @@ import {
FileText, FileText,
ChevronDown, ChevronDown,
ChevronUp, ChevronUp,
ChevronRight,
Target, Target,
Activity, Activity,
BarChart3, BarChart3,
TrendingUp, TrendingUp,
ArrowRight ArrowRight,
Folder,
FolderOpen,
Maximize2,
X
} from 'lucide-react'; } from 'lucide-react';
import { useStudy } from '../context/StudyContext'; import { useStudy } from '../context/StudyContext';
import { Study } from '../types'; import { Study } from '../types';
@@ -28,8 +33,64 @@ const Home: React.FC = () => {
const [readmeLoading, setReadmeLoading] = useState(false); const [readmeLoading, setReadmeLoading] = useState(false);
const [sortField, setSortField] = useState<'name' | 'status' | 'trials' | 'bestValue'>('trials'); const [sortField, setSortField] = useState<'name' | 'status' | 'trials' | 'bestValue'>('trials');
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc'); const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc');
const [expandedTopics, setExpandedTopics] = useState<Set<string>>(new Set());
const [isFullscreen, setIsFullscreen] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
// Group studies by topic, sorted by most recent first
const studiesByTopic = useMemo(() => {
const grouped: Record<string, Study[]> = {};
studies.forEach(study => {
const topic = study.topic || 'Other';
if (!grouped[topic]) grouped[topic] = [];
grouped[topic].push(study);
});
// Sort studies within each topic by last_modified (most recent first)
Object.keys(grouped).forEach(topic => {
grouped[topic].sort((a, b) => {
const aTime = a.last_modified ? new Date(a.last_modified).getTime() : 0;
const bTime = b.last_modified ? new Date(b.last_modified).getTime() : 0;
return bTime - aTime; // Descending (most recent first)
});
});
// Get most recent study time for each topic (for topic sorting)
const topicMostRecent: Record<string, number> = {};
Object.keys(grouped).forEach(topic => {
const mostRecent = grouped[topic][0]?.last_modified;
topicMostRecent[topic] = mostRecent ? new Date(mostRecent).getTime() : 0;
});
// Sort topics by most recent study (most recent first), 'Other' always last
const sortedTopics = Object.keys(grouped).sort((a, b) => {
if (a === 'Other') return 1;
if (b === 'Other') return -1;
return topicMostRecent[b] - topicMostRecent[a]; // Descending (most recent first)
});
const result: Record<string, Study[]> = {};
sortedTopics.forEach(topic => {
result[topic] = grouped[topic];
});
return result;
}, [studies]);
// Topics start collapsed by default - no initialization needed
// Users can expand topics by clicking on them
const toggleTopic = (topic: string) => {
setExpandedTopics(prev => {
const next = new Set(prev);
if (next.has(topic)) {
next.delete(topic);
} else {
next.add(topic);
}
return next;
});
};
// Load README when a study is selected for preview // Load README when a study is selected for preview
useEffect(() => { useEffect(() => {
if (selectedPreview) { if (selectedPreview) {
@@ -235,113 +296,105 @@ const Home: React.FC = () => {
<p className="text-sm mt-1 text-dark-500">Create a new study to get started</p> <p className="text-sm mt-1 text-dark-500">Create a new study to get started</p>
</div> </div>
) : ( ) : (
<div className="overflow-x-auto max-h-[500px] overflow-y-auto"> <div className="max-h-[500px] overflow-y-auto">
<table className="w-full"> {Object.entries(studiesByTopic).map(([topic, topicStudies]) => {
<thead className="sticky top-0 bg-dark-750 z-10"> const isExpanded = expandedTopics.has(topic);
<tr className="border-b border-dark-600"> const topicTrials = topicStudies.reduce((sum, s) => sum + s.progress.current, 0);
<th const runningCount = topicStudies.filter(s => s.status === 'running').length;
className="text-left py-3 px-4 text-dark-400 font-medium cursor-pointer hover:text-white transition-colors"
onClick={() => handleSort('name')}
>
<div className="flex items-center gap-1">
Study Name
{sortField === 'name' && (
sortDir === 'asc' ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />
)}
</div>
</th>
<th
className="text-left py-3 px-4 text-dark-400 font-medium cursor-pointer hover:text-white transition-colors"
onClick={() => handleSort('status')}
>
<div className="flex items-center gap-1">
Status
{sortField === 'status' && (
sortDir === 'asc' ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />
)}
</div>
</th>
<th
className="text-left py-3 px-4 text-dark-400 font-medium cursor-pointer hover:text-white transition-colors"
onClick={() => handleSort('trials')}
>
<div className="flex items-center gap-1">
Progress
{sortField === 'trials' && (
sortDir === 'asc' ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />
)}
</div>
</th>
<th
className="text-left py-3 px-4 text-dark-400 font-medium cursor-pointer hover:text-white transition-colors"
onClick={() => handleSort('bestValue')}
>
<div className="flex items-center gap-1">
Best
{sortField === 'bestValue' && (
sortDir === 'asc' ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />
)}
</div>
</th>
</tr>
</thead>
<tbody>
{sortedStudies.map((study) => {
const completionPercent = study.progress.total > 0
? Math.round((study.progress.current / study.progress.total) * 100)
: 0;
return ( return (
<tr <div key={topic} className="border-b border-dark-600 last:border-b-0">
key={study.id} {/* Topic Header */}
onClick={() => setSelectedPreview(study)} <button
className={`border-b border-dark-700 hover:bg-dark-750 transition-colors cursor-pointer ${ onClick={() => toggleTopic(topic)}
selectedPreview?.id === study.id ? 'bg-primary-900/20' : '' className="w-full px-4 py-3 flex items-center justify-between hover:bg-dark-750 transition-colors"
}`} >
> <div className="flex items-center gap-3">
<td className="py-3 px-4"> {isExpanded ? (
<div className="flex flex-col"> <FolderOpen className="w-5 h-5 text-primary-400" />
<span className="text-white font-medium truncate max-w-[200px]"> ) : (
{study.name || study.id} <Folder className="w-5 h-5 text-dark-400" />
</span> )}
{study.name && ( <span className="text-white font-medium">{topic.replace(/_/g, ' ')}</span>
<span className="text-xs text-dark-500 truncate max-w-[200px]">{study.id}</span> <span className="text-dark-500 text-sm">({topicStudies.length})</span>
)} {runningCount > 0 && (
</div> <span className="flex items-center gap-1 text-xs text-green-400 bg-green-500/10 px-2 py-0.5 rounded-full">
</td> <Play className="w-3 h-3" />
<td className="py-3 px-4"> {runningCount} running
<span className={`inline-flex items-center gap-1.5 px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(study.status)}`}>
{getStatusIcon(study.status)}
{study.status}
</span> </span>
</td> )}
<td className="py-3 px-4"> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-4">
<div className="flex-1 h-2 bg-dark-600 rounded-full overflow-hidden max-w-[80px]"> <span className="text-dark-400 text-sm">{topicTrials.toLocaleString()} trials</span>
<div {isExpanded ? (
className={`h-full transition-all ${ <ChevronDown className="w-4 h-4 text-dark-400" />
completionPercent >= 100 ? 'bg-green-500' : ) : (
completionPercent >= 50 ? 'bg-primary-500' : <ChevronRight className="w-4 h-4 text-dark-400" />
'bg-yellow-500' )}
}`} </div>
style={{ width: `${Math.min(completionPercent, 100)}%` }} </button>
/>
{/* Topic Studies */}
{isExpanded && (
<div className="bg-dark-850">
{topicStudies.map((study) => {
const completionPercent = study.progress.total > 0
? Math.round((study.progress.current / study.progress.total) * 100)
: 0;
return (
<div
key={study.id}
onClick={() => setSelectedPreview(study)}
className={`px-4 py-3 pl-12 flex items-center gap-4 border-t border-dark-700 hover:bg-dark-700 transition-colors cursor-pointer ${
selectedPreview?.id === study.id ? 'bg-primary-900/20' : ''
}`}
>
{/* Study Name */}
<div className="flex-1 min-w-0">
<span className="text-white font-medium truncate block">
{study.name || study.id}
</span>
{study.name && (
<span className="text-xs text-dark-500 truncate block">{study.id}</span>
)}
</div>
{/* Status */}
<span className={`inline-flex items-center gap-1.5 px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(study.status)}`}>
{getStatusIcon(study.status)}
{study.status}
</span>
{/* Progress */}
<div className="flex items-center gap-2 w-32">
<div className="flex-1 h-2 bg-dark-600 rounded-full overflow-hidden">
<div
className={`h-full transition-all ${
completionPercent >= 100 ? 'bg-green-500' :
completionPercent >= 50 ? 'bg-primary-500' :
'bg-yellow-500'
}`}
style={{ width: `${Math.min(completionPercent, 100)}%` }}
/>
</div>
<span className="text-dark-400 text-xs font-mono w-14 text-right">
{study.progress.current}/{study.progress.total}
</span>
</div>
{/* Best Value */}
<span className={`font-mono text-sm w-20 text-right ${study.best_value !== null ? 'text-primary-400' : 'text-dark-500'}`}>
{study.best_value !== null ? study.best_value.toExponential(2) : 'N/A'}
</span>
</div> </div>
<span className="text-dark-400 text-sm font-mono w-16"> );
{study.progress.current}/{study.progress.total} })}
</span> </div>
</div> )}
</td> </div>
<td className="py-3 px-4"> );
<span className={`font-mono text-sm ${study.best_value !== null ? 'text-primary-400' : 'text-dark-500'}`}> })}
{study.best_value !== null ? study.best_value.toExponential(3) : 'N/A'}
</span>
</td>
</tr>
);
})}
</tbody>
</table>
</div> </div>
)} )}
</div> </div>
@@ -375,21 +428,30 @@ const Home: React.FC = () => {
</div> </div>
{/* Study Quick Stats */} {/* Study Quick Stats */}
<div className="px-6 py-3 border-b border-dark-600 flex items-center gap-6 text-sm"> <div className="px-6 py-3 border-b border-dark-600 flex items-center justify-between text-sm">
<div className="flex items-center gap-2"> <div className="flex items-center gap-6">
{getStatusIcon(selectedPreview.status)} <div className="flex items-center gap-2">
<span className="text-dark-300 capitalize">{selectedPreview.status}</span> {getStatusIcon(selectedPreview.status)}
</div> <span className="text-dark-300 capitalize">{selectedPreview.status}</span>
<div className="flex items-center gap-2 text-dark-400">
<Activity className="w-4 h-4" />
<span>{selectedPreview.progress.current} / {selectedPreview.progress.total} trials</span>
</div>
{selectedPreview.best_value !== null && (
<div className="flex items-center gap-2 text-primary-400">
<Target className="w-4 h-4" />
<span>Best: {selectedPreview.best_value.toExponential(4)}</span>
</div> </div>
)} <div className="flex items-center gap-2 text-dark-400">
<Activity className="w-4 h-4" />
<span>{selectedPreview.progress.current} / {selectedPreview.progress.total} trials</span>
</div>
{selectedPreview.best_value !== null && (
<div className="flex items-center gap-2 text-primary-400">
<Target className="w-4 h-4" />
<span>Best: {selectedPreview.best_value.toExponential(4)}</span>
</div>
)}
</div>
<button
onClick={() => setIsFullscreen(true)}
className="p-2 hover:bg-dark-700 rounded-lg transition-colors text-dark-400 hover:text-white"
title="View fullscreen"
>
<Maximize2 className="w-4 h-4" />
</button>
</div> </div>
{/* README Content */} {/* README Content */}
@@ -400,7 +462,7 @@ const Home: React.FC = () => {
Loading documentation... Loading documentation...
</div> </div>
) : ( ) : (
<MarkdownRenderer content={readme} /> <MarkdownRenderer content={readme} studyId={selectedPreview.id} />
)} )}
</div> </div>
</> </>
@@ -416,6 +478,59 @@ const Home: React.FC = () => {
</div> </div>
</div> </div>
</main> </main>
{/* Fullscreen README Modal */}
{isFullscreen && selectedPreview && (
<div className="fixed inset-0 z-50 bg-dark-900/95 backdrop-blur-sm overflow-hidden">
<div className="h-full flex flex-col">
{/* Modal Header */}
<div className="flex-shrink-0 px-8 py-4 border-b border-dark-700 flex items-center justify-between bg-dark-800">
<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>
<div className="flex items-center gap-3">
<button
onClick={() => handleSelectStudy(selectedPreview)}
className="flex items-center gap-2 px-4 py-2 bg-primary-600 hover:bg-primary-500
text-white rounded-lg transition-all font-medium"
>
Open Study
<ArrowRight className="w-4 h-4" />
</button>
<button
onClick={() => setIsFullscreen(false)}
className="p-2 hover:bg-dark-700 rounded-lg transition-colors text-dark-400 hover:text-white"
title="Close fullscreen"
>
<X className="w-5 h-5" />
</button>
</div>
</div>
{/* Modal Content */}
<div className="flex-1 overflow-y-auto p-8">
<div className="max-w-4xl mx-auto">
{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>
) : (
<MarkdownRenderer content={readme} studyId={selectedPreview.id} />
)}
</div>
</div>
</div>
</div>
)}
</div> </div>
); );
}; };

View File

@@ -295,7 +295,7 @@ export default function Results() {
const visibleParams = showAllParams ? paramEntries : paramEntries.slice(0, 6); const visibleParams = showAllParams ? paramEntries : paramEntries.slice(0, 6);
return ( return (
<div className="h-full flex flex-col max-w-[2400px] mx-auto px-4"> <div className="h-full flex flex-col w-full">
{/* Header */} {/* Header */}
<header className="mb-6 flex items-center justify-between border-b border-dark-600 pb-4"> <header className="mb-6 flex items-center justify-between border-b border-dark-600 pb-4">
<div> <div>

View File

@@ -249,7 +249,7 @@ export default function Setup() {
}, 1) || 0; }, 1) || 0;
return ( return (
<div className="w-full max-w-[2400px] mx-auto px-4"> <div className="w-full">
{/* Header */} {/* Header */}
<header className="mb-6 flex items-center justify-between border-b border-dark-600 pb-4"> <header className="mb-6 flex items-center justify-between border-b border-dark-600 pb-4">
<div> <div>

View File

@@ -2,6 +2,7 @@
export interface Study { export interface Study {
id: string; id: string;
name: string; name: string;
topic: string | null; // Topic folder name for grouping (e.g., 'M1_Mirror', 'Simple_Bracket')
status: 'not_started' | 'running' | 'paused' | 'completed'; status: 'not_started' | 'running' | 'paused' | 'completed';
progress: { progress: {
current: number; current: number;

View File

@@ -10,7 +10,7 @@ export default defineConfig({
strictPort: false, // Allow fallback to next available port strictPort: false, // Allow fallback to next available port
proxy: { proxy: {
'/api': { '/api': {
target: 'http://127.0.0.1:8000', // Use 127.0.0.1 instead of localhost target: 'http://127.0.0.1:8000', // Backend port
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
ws: true, ws: true,

View File

@@ -14,8 +14,8 @@ import os
# NX Installation Directory # NX Installation Directory
# Change this to update NX version across entire Atomizer codebase # Change this to update NX version across entire Atomizer codebase
NX_VERSION = "2506" NX_VERSION = "2512"
NX_INSTALLATION_DIR = Path(f"C:/Program Files/Siemens/NX{NX_VERSION}") NX_INSTALLATION_DIR = Path("C:/Program Files/Siemens/DesigncenterNX2512")
# Derived NX Paths (automatically updated when NX_VERSION changes) # Derived NX Paths (automatically updated when NX_VERSION changes)
NX_BIN_DIR = NX_INSTALLATION_DIR / "NXBIN" NX_BIN_DIR = NX_INSTALLATION_DIR / "NXBIN"

View File

@@ -27,9 +27,9 @@ class Colors:
def print_banner(): def print_banner():
print(f""" print(f"""
{Colors.BLUE}{Colors.BOLD}╔═══════════════════════════════════════════╗ {Colors.BLUE}{Colors.BOLD}============================================
ATOMIZER DASHBOARD LAUNCHER ATOMIZER DASHBOARD LAUNCHER
╚═══════════════════════════════════════════╝{Colors.END} ============================================{Colors.END}
""") """)
def main(): def main():
@@ -50,11 +50,12 @@ def main():
processes = [] processes = []
try: try:
# Start backend # Start backend - use conda run to ensure atomizer environment
print(f"{Colors.YELLOW}Starting backend server (FastAPI on port 8000)...{Colors.END}") print(f"{Colors.YELLOW}Starting backend server (FastAPI on port 8000)...{Colors.END}")
backend_proc = subprocess.Popen( backend_proc = subprocess.Popen(
["python", "-m", "uvicorn", "api.main:app", "--reload", "--port", "8000"], ["conda", "run", "-n", "atomizer", "python", "-m", "uvicorn", "api.main:app", "--port", "8000"],
cwd=str(backend_dir), cwd=str(backend_dir),
shell=True,
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP if sys.platform == "win32" else 0 creationflags=subprocess.CREATE_NEW_PROCESS_GROUP if sys.platform == "win32" else 0
) )
processes.append(("Backend", backend_proc)) processes.append(("Backend", backend_proc))