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:
@@ -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": []
|
||||||
|
|||||||
16
CLAUDE.md
16
CLAUDE.md
@@ -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
|
||||||
|
|||||||
@@ -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 []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 [];
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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';
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}`;
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
Reference in New Issue
Block a user