feat: Add dashboard chat integration and MCP server

Major changes:
- Dashboard: WebSocket-based chat with session management
- Dashboard: New chat components (ChatPane, ChatInput, ModeToggle)
- Dashboard: Enhanced UI with parallel coordinates chart
- MCP Server: New atomizer-tools server for Claude integration
- Extractors: Enhanced Zernike OPD extractor
- Reports: Improved report generator

New studies (configs and scripts only):
- M1 Mirror: Cost reduction campaign studies
- Simple Beam, Simple Bracket, UAV Arm studies

Note: Large iteration data (2_iterations/, best_design_archive/)
excluded via .gitignore - kept on local Gitea only.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-13 15:53:55 -05:00
parent 69c0d76b50
commit 73a7b9d9f1
1680 changed files with 144922 additions and 723 deletions

View File

@@ -550,3 +550,158 @@ async def get_insights_summary(study_id: str):
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/studies/{study_id}/new-best")
async def check_new_best(study_id: str):
"""Check if there's a new best solution that needs insight generation.
The optimization script writes new_best.json when a new best is found.
Dashboard can poll this endpoint to auto-generate insights.
"""
try:
study_path = resolve_study_path(study_id)
new_best_file = study_path / "3_results" / "new_best.json"
if not new_best_file.exists():
return {
"study_id": study_id,
"has_new_best": False,
"new_best": None
}
with open(new_best_file) as f:
new_best = json.load(f)
return {
"study_id": study_id,
"has_new_best": new_best.get("needs_insights", False),
"new_best": new_best
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/studies/{study_id}/auto-insights")
async def generate_auto_insights(study_id: str):
"""Generate insights for the current best solution based on config.
Reads insights config from optimization_config.json and generates
all enabled insights for the best (or latest new best) iteration.
Clears the needs_insights flag after generation.
"""
try:
study_path = resolve_study_path(study_id)
# Find the iteration to generate insights for
new_best_file = study_path / "3_results" / "new_best.json"
iteration_id = None
if new_best_file.exists():
with open(new_best_file) as f:
new_best = json.load(f)
iteration_id = new_best.get("iteration_folder")
if not iteration_id:
# Fall back to finding best from database or latest iteration
iter_dir = study_path / "2_iterations"
if iter_dir.exists():
iterations = sorted(
[d for d in iter_dir.iterdir() if d.is_dir() and any(d.glob("*.op2"))],
key=lambda p: p.stat().st_mtime,
reverse=True
)
if iterations:
iteration_id = iterations[0].name
if not iteration_id:
raise HTTPException(status_code=400, detail="No iteration found for insight generation")
# Load insights config
config_path = study_path / "1_setup" / "optimization_config.json"
if not config_path.exists():
config_path = study_path / "optimization_config.json"
if not config_path.exists():
raise HTTPException(status_code=400, detail="No optimization_config.json found")
with open(config_path) as f:
config = json.load(f)
insights_config = config.get("insights", [])
if not insights_config:
return {
"study_id": study_id,
"message": "No insights configured in optimization_config.json",
"results": []
}
# Generate insights
from optimization_engine.insights import get_insight, InsightConfig
iter_path = study_path / "2_iterations" / iteration_id
op2_files = list(iter_path.glob("*.op2"))
if not op2_files:
raise HTTPException(status_code=400, detail=f"No OP2 file in {iteration_id}")
op2_path = op2_files[0]
results = []
for insight_spec in insights_config:
if not insight_spec.get("enabled", True):
continue
insight_type = insight_spec.get("type")
insight_name = insight_spec.get("name", insight_type)
try:
insight = get_insight(insight_type, study_path)
if insight:
insight.op2_path = op2_path
config_obj = InsightConfig(extra=insight_spec.get("config", {}))
result = insight.generate(config_obj)
results.append({
"type": insight_type,
"name": insight_name,
"success": result.success,
"html_path": str(result.html_path) if result.html_path else None,
"error": result.error
})
except Exception as e:
results.append({
"type": insight_type,
"name": insight_name,
"success": False,
"error": str(e)
})
# Clear the needs_insights flag
if new_best_file.exists():
with open(new_best_file) as f:
new_best = json.load(f)
new_best["needs_insights"] = False
with open(new_best_file, 'w') as f:
json.dump(new_best, f, indent=2)
return {
"study_id": study_id,
"iteration": iteration_id,
"results": results,
"summary": {
"total": len(results),
"successful": sum(1 for r in results if r["success"]),
"failed": sum(1 for r in results if not r["success"])
}
}
except HTTPException:
raise
except ImportError as e:
raise HTTPException(status_code=500, detail=f"Insights module not available: {str(e)}")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))