refactor: Archive experimental LLM features for MVP stability (Phase 1.1)

Moved experimental LLM integration code to optimization_engine/future/:
- llm_optimization_runner.py - Runtime LLM API runner
- llm_workflow_analyzer.py - Workflow analysis
- inline_code_generator.py - Auto-generate calculations
- hook_generator.py - Auto-generate hooks
- report_generator.py - LLM report generation
- extractor_orchestrator.py - Extractor orchestration

Added comprehensive optimization_engine/future/README.md explaining:
- MVP LLM strategy (Claude Code skills, not runtime LLM)
- Why files were archived
- When to revisit post-MVP
- Production architecture reference

Production runner confirmed: optimization_engine/runner.py is sole active runner.

This establishes clear separation between:
- Production code (stable, no runtime LLM dependencies)
- Experimental code (archived for post-MVP exploration)

Part of Phase 1: Core Stabilization & Organization for MVP

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-24 09:12:36 -05:00
parent 46515475cb
commit d228ccec66
377 changed files with 1195 additions and 16789 deletions

View File

@@ -3,12 +3,15 @@ Optimization API endpoints
Handles study status, history retrieval, and control operations
"""
from fastapi import APIRouter, HTTPException
from fastapi import APIRouter, HTTPException, UploadFile, File, Form
from fastapi.responses import JSONResponse, FileResponse
from pathlib import Path
from typing import List, Dict, Optional
import json
import sys
import sqlite3
import shutil
from datetime import datetime
# Add project root to path
sys.path.append(str(Path(__file__).parent.parent.parent.parent.parent))
@@ -307,12 +310,40 @@ async def get_optimization_history(study_id: str, limit: Optional[int] = None):
except (ValueError, TypeError):
params[param_name] = param_value
# Get user attributes (extracted results: mass, frequency, stress, displacement, etc.)
cursor.execute("""
SELECT key, value_json
FROM trial_user_attributes
WHERE trial_id = ?
""", (trial_id,))
user_attrs = {}
for key, value_json in cursor.fetchall():
try:
user_attrs[key] = json.loads(value_json)
except (ValueError, TypeError):
user_attrs[key] = value_json
# Extract relevant metrics for results (mass, frequency, stress, displacement, etc.)
results = {}
if "mass" in user_attrs:
results["mass"] = user_attrs["mass"]
if "frequency" in user_attrs:
results["frequency"] = user_attrs["frequency"]
if "max_stress" in user_attrs:
results["max_stress"] = user_attrs["max_stress"]
if "max_displacement" in user_attrs:
results["max_displacement"] = user_attrs["max_displacement"]
# Fallback to first frequency from objectives if available
if not results and len(values) > 0:
results["first_frequency"] = values[0]
trials.append({
"trial_number": trial_num,
"objective": values[0] if len(values) > 0 else None, # Primary objective
"objectives": values if len(values) > 1 else None, # All objectives for multi-objective
"design_variables": params,
"results": {"first_frequency": values[0]} if len(values) > 0 else {},
"results": results,
"user_attrs": user_attrs, # Include all user attributes
"start_time": start_time,
"end_time": end_time
})
@@ -488,3 +519,268 @@ async def get_pareto_front(study_id: str):
raise HTTPException(status_code=404, detail=f"Study {study_id} not found")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to get Pareto front: {str(e)}")
@router.post("/studies")
async def create_study(
config: str = Form(...),
prt_file: Optional[UploadFile] = File(None),
sim_file: Optional[UploadFile] = File(None),
fem_file: Optional[UploadFile] = File(None)
):
"""
Create a new optimization study
Accepts:
- config: JSON string with study configuration
- prt_file: NX part file (optional if using existing study)
- sim_file: NX simulation file (optional)
- fem_file: NX FEM file (optional)
"""
try:
# Parse config
config_data = json.loads(config)
study_name = config_data.get("name") # Changed from study_name to name to match frontend
if not study_name:
raise HTTPException(status_code=400, detail="name is required in config")
# Create study directory structure
study_dir = STUDIES_DIR / study_name
if study_dir.exists():
raise HTTPException(status_code=400, detail=f"Study {study_name} already exists")
setup_dir = study_dir / "1_setup"
model_dir = setup_dir / "model"
results_dir = study_dir / "2_results"
setup_dir.mkdir(parents=True, exist_ok=True)
model_dir.mkdir(parents=True, exist_ok=True)
results_dir.mkdir(parents=True, exist_ok=True)
# Save config file
config_file = setup_dir / "optimization_config.json"
with open(config_file, 'w') as f:
json.dump(config_data, f, indent=2)
# Save uploaded files
files_saved = {}
if prt_file:
prt_path = model_dir / prt_file.filename
with open(prt_path, 'wb') as f:
content = await prt_file.read()
f.write(content)
files_saved['prt_file'] = str(prt_path)
if sim_file:
sim_path = model_dir / sim_file.filename
with open(sim_path, 'wb') as f:
content = await sim_file.read()
f.write(content)
files_saved['sim_file'] = str(sim_path)
if fem_file:
fem_path = model_dir / fem_file.filename
with open(fem_path, 'wb') as f:
content = await fem_file.read()
f.write(content)
files_saved['fem_file'] = str(fem_path)
return JSONResponse(
status_code=201,
content={
"status": "created",
"study_id": study_name,
"study_path": str(study_dir),
"config_path": str(config_file),
"files_saved": files_saved,
"message": f"Study {study_name} created successfully. Ready to run optimization."
}
)
except json.JSONDecodeError as e:
raise HTTPException(status_code=400, detail=f"Invalid JSON in config: {str(e)}")
except Exception as e:
# Clean up on error
if 'study_dir' in locals() and study_dir.exists():
shutil.rmtree(study_dir)
raise HTTPException(status_code=500, detail=f"Failed to create study: {str(e)}")
@router.post("/studies/{study_id}/convert-mesh")
async def convert_study_mesh(study_id: str):
"""
Convert study mesh to GLTF for 3D visualization
Creates a web-viewable 3D model with FEA results as vertex colors
"""
try:
study_dir = STUDIES_DIR / study_id
if not study_dir.exists():
raise HTTPException(status_code=404, detail=f"Study {study_id} not found")
# Import mesh converter
sys.path.append(str(Path(__file__).parent.parent.parent.parent.parent))
from optimization_engine.mesh_converter import convert_study_mesh
# Convert mesh
output_path = convert_study_mesh(study_dir)
if output_path and output_path.exists():
return {
"status": "success",
"gltf_path": str(output_path),
"gltf_url": f"/api/optimization/studies/{study_id}/mesh/model.gltf",
"metadata_url": f"/api/optimization/studies/{study_id}/mesh/model.json",
"message": "Mesh converted successfully"
}
else:
raise HTTPException(status_code=500, detail="Mesh conversion failed")
except FileNotFoundError:
raise HTTPException(status_code=404, detail=f"Study {study_id} not found")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to convert mesh: {str(e)}")
@router.get("/studies/{study_id}/mesh/{filename}")
async def get_mesh_file(study_id: str, filename: str):
"""
Serve GLTF mesh files and metadata
Supports .gltf, .bin, and .json files
"""
try:
# Validate filename to prevent directory traversal
if '..' in filename or '/' in filename or '\\' in filename:
raise HTTPException(status_code=400, detail="Invalid filename")
study_dir = STUDIES_DIR / study_id
visualization_dir = study_dir / "3_visualization"
file_path = visualization_dir / filename
if not file_path.exists():
raise HTTPException(status_code=404, detail=f"File {filename} not found")
# Determine content type
suffix = file_path.suffix.lower()
content_types = {
'.gltf': 'model/gltf+json',
'.bin': 'application/octet-stream',
'.json': 'application/json',
'.glb': 'model/gltf-binary'
}
content_type = content_types.get(suffix, 'application/octet-stream')
return FileResponse(
path=str(file_path),
media_type=content_type,
filename=filename
)
except FileNotFoundError:
raise HTTPException(status_code=404, detail=f"File not found")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to serve mesh file: {str(e)}")
@router.post("/studies/{study_id}/generate-report")
async def generate_report(
study_id: str,
format: str = "markdown",
include_llm_summary: bool = False
):
"""
Generate an optimization report in the specified format
Args:
study_id: Study identifier
format: Report format ('markdown', 'html', or 'pdf')
include_llm_summary: Whether to include LLM-generated executive summary
Returns:
Information about the generated report including download URL
"""
try:
study_dir = STUDIES_DIR / study_id
if not study_dir.exists():
raise HTTPException(status_code=404, detail=f"Study {study_id} not found")
# Validate format
valid_formats = ['markdown', 'md', 'html', 'pdf']
if format.lower() not in valid_formats:
raise HTTPException(status_code=400, detail=f"Invalid format. Must be one of: {', '.join(valid_formats)}")
# Import report generator
sys.path.append(str(Path(__file__).parent.parent.parent.parent.parent))
from optimization_engine.report_generator import generate_study_report
# Generate report
output_path = generate_study_report(
study_dir=study_dir,
output_format=format.lower(),
include_llm_summary=include_llm_summary
)
if output_path and output_path.exists():
# Get relative path for URL
rel_path = output_path.relative_to(study_dir)
return {
"status": "success",
"format": format,
"file_path": str(output_path),
"download_url": f"/api/optimization/studies/{study_id}/reports/{output_path.name}",
"file_size": output_path.stat().st_size,
"message": f"Report generated successfully in {format} format"
}
else:
raise HTTPException(status_code=500, detail="Report generation failed")
except FileNotFoundError:
raise HTTPException(status_code=404, detail=f"Study {study_id} not found")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to generate report: {str(e)}")
@router.get("/studies/{study_id}/reports/{filename}")
async def download_report(study_id: str, filename: str):
"""
Download a generated report file
Args:
study_id: Study identifier
filename: Report filename
Returns:
Report file for download
"""
try:
# Validate filename to prevent directory traversal
if '..' in filename or '/' in filename or '\\' in filename:
raise HTTPException(status_code=400, detail="Invalid filename")
study_dir = STUDIES_DIR / study_id
results_dir = study_dir / "2_results"
file_path = results_dir / filename
if not file_path.exists():
raise HTTPException(status_code=404, detail=f"Report file {filename} not found")
# Determine content type
suffix = file_path.suffix.lower()
content_types = {
'.md': 'text/markdown',
'.html': 'text/html',
'.pdf': 'application/pdf',
'.json': 'application/json'
}
content_type = content_types.get(suffix, 'application/octet-stream')
return FileResponse(
path=str(file_path),
media_type=content_type,
filename=filename,
headers={"Content-Disposition": f"attachment; filename={filename}"}
)
except FileNotFoundError:
raise HTTPException(status_code=404, detail=f"Report file not found")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to download report: {str(e)}")

View File

@@ -3,33 +3,53 @@ import {
LineChart, Line, ScatterChart, Scatter,
XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, Cell
} from 'recharts';
import { useWebSocket } from '../hooks/useWebSocket';
import { Card } from '../components/Card';
import { MetricCard } from '../components/MetricCard';
import { StudyCard } from '../components/StudyCard';
import { useOptimizationWebSocket } from '../hooks/useWebSocket';
import { apiClient } from '../api/client';
import { Card } from '../components/common/Card';
import { MetricCard } from '../components/dashboard/MetricCard';
import { StudyCard } from '../components/dashboard/StudyCard';
import { OptimizerPanel } from '../components/OptimizerPanel';
import { ParetoPlot } from '../components/ParetoPlot';
import { ParallelCoordinatesPlot } from '../components/ParallelCoordinatesPlot';
import type { Study, Trial, ConvergenceDataPoint, ParameterSpaceDataPoint } from '../types';
interface DashboardProps {
studies: Study[];
selectedStudyId: string | null;
onStudySelect: (studyId: string) => void;
}
export default function Dashboard({ studies, selectedStudyId, onStudySelect }: DashboardProps) {
const [trials, setTrials] = useState<Trial[]>([]);
export default function Dashboard() {
const [studies, setStudies] = useState<Study[]>([]);
const [selectedStudyId, setSelectedStudyId] = useState<string | null>(null);
const [allTrials, setAllTrials] = useState<Trial[]>([]);
const [displayedTrials, setDisplayedTrials] = useState<Trial[]>([]);
const [bestValue, setBestValue] = useState<number>(Infinity);
const [prunedCount, setPrunedCount] = useState<number>(0);
const [alerts, setAlerts] = useState<Array<{ id: number; type: 'success' | 'warning'; message: string }>>([]);
const [alertIdCounter, setAlertIdCounter] = useState(0);
const [expandedTrials, setExpandedTrials] = useState<Set<number>>(new Set());
const [sortBy, setSortBy] = useState<'performance' | 'chronological'>('performance');
// Protocol 13: New state for metadata and Pareto front
const [studyMetadata, setStudyMetadata] = useState<any>(null);
const [paretoFront, setParetoFront] = useState<any[]>([]);
// Load studies on mount
useEffect(() => {
apiClient.getStudies()
.then(data => {
setStudies(data.studies);
if (data.studies.length > 0) {
// Check LocalStorage for last selected study
const savedStudyId = localStorage.getItem('lastSelectedStudyId');
const studyExists = data.studies.find(s => s.id === savedStudyId);
if (savedStudyId && studyExists) {
setSelectedStudyId(savedStudyId);
} else {
const running = data.studies.find(s => s.status === 'running');
setSelectedStudyId(running?.id || data.studies[0].id);
}
}
})
.catch(console.error);
}, []);
const showAlert = (type: 'success' | 'warning', message: string) => {
const id = alertIdCounter;
setAlertIdCounter(prev => prev + 1);
@@ -40,54 +60,50 @@ export default function Dashboard({ studies, selectedStudyId, onStudySelect }: D
};
// WebSocket connection
const { isConnected } = useWebSocket({
const { connectionStatus } = useOptimizationWebSocket({
studyId: selectedStudyId,
onTrialCompleted: (trial) => {
setTrials(prev => [trial, ...prev].slice(0, 20));
setAllTrials(prev => [...prev, trial]);
if (trial.objective < bestValue) {
setBestValue(trial.objective);
showAlert('success', `New best: ${trial.objective.toFixed(4)} (Trial #${trial.trial_number})`);
onMessage: (msg) => {
if (msg.type === 'trial_completed') {
const trial = msg.data as Trial;
setAllTrials(prev => [...prev, trial]);
if (trial.objective !== null && trial.objective !== undefined && trial.objective < bestValue) {
setBestValue(trial.objective);
showAlert('success', `New best: ${trial.objective.toFixed(4)} (Trial #${trial.trial_number})`);
}
} else if (msg.type === 'trial_pruned') {
setPrunedCount(prev => prev + 1);
showAlert('warning', `Trial pruned: ${msg.data.pruning_cause}`);
}
},
onNewBest: (trial) => {
console.log('New best trial:', trial);
},
onTrialPruned: (pruned) => {
setPrunedCount(prev => prev + 1);
showAlert('warning', `Trial #${pruned.trial_number} pruned: ${pruned.pruning_cause}`);
},
}
});
// Load initial trial history when study changes
useEffect(() => {
if (selectedStudyId) {
setTrials([]);
setAllTrials([]);
setBestValue(Infinity);
setPrunedCount(0);
setExpandedTrials(new Set());
// Fetch full history
fetch(`/api/optimization/studies/${selectedStudyId}/history`)
.then(res => res.json())
// Save to LocalStorage
localStorage.setItem('lastSelectedStudyId', selectedStudyId);
apiClient.getStudyHistory(selectedStudyId)
.then(data => {
const sortedTrials = data.trials.sort((a: Trial, b: Trial) => a.trial_number - b.trial_number);
setAllTrials(sortedTrials);
setTrials(sortedTrials.slice(-20).reverse());
if (sortedTrials.length > 0) {
const minObj = Math.min(...sortedTrials.map((t: Trial) => t.objective));
const validTrials = data.trials.filter(t => t.objective !== null && t.objective !== undefined);
setAllTrials(validTrials);
if (validTrials.length > 0) {
const minObj = Math.min(...validTrials.map(t => t.objective));
setBestValue(minObj);
}
})
.catch(err => console.error('Failed to load history:', err));
.catch(console.error);
// Fetch pruning count
fetch(`/api/optimization/studies/${selectedStudyId}/pruning`)
.then(res => res.json())
apiClient.getStudyPruning(selectedStudyId)
.then(data => {
setPrunedCount(data.pruned_trials?.length || 0);
})
.catch(err => console.error('Failed to load pruning data:', err));
.catch(console.error);
// Protocol 13: Fetch metadata
fetch(`/api/optimization/studies/${selectedStudyId}/metadata`)
@@ -97,12 +113,12 @@ export default function Dashboard({ studies, selectedStudyId, onStudySelect }: D
})
.catch(err => console.error('Failed to load metadata:', err));
// Protocol 13: Fetch Pareto front
// Protocol 13: Fetch Pareto front (raw format for Protocol 13 components)
fetch(`/api/optimization/studies/${selectedStudyId}/pareto-front`)
.then(res => res.json())
.then(data => {
if (data.is_multi_objective) {
setParetoFront(data.pareto_front);
.then(paretoData => {
if (paretoData.is_multi_objective && paretoData.pareto_front) {
setParetoFront(paretoData.pareto_front);
} else {
setParetoFront([]);
}
@@ -111,42 +127,92 @@ export default function Dashboard({ studies, selectedStudyId, onStudySelect }: D
}
}, [selectedStudyId]);
// Prepare chart data
const convergenceData: ConvergenceDataPoint[] = allTrials.map((trial, idx) => ({
trial_number: trial.trial_number,
objective: trial.objective,
best_so_far: Math.min(...allTrials.slice(0, idx + 1).map(t => t.objective)),
}));
// Sort trials based on selected sort order
useEffect(() => {
let sorted = [...allTrials];
if (sortBy === 'performance') {
// Sort by objective (best first)
sorted.sort((a, b) => {
const aObj = a.objective ?? Infinity;
const bObj = b.objective ?? Infinity;
return aObj - bObj;
});
} else {
// Chronological (newest first)
sorted.sort((a, b) => b.trial_number - a.trial_number);
}
setDisplayedTrials(sorted);
}, [allTrials, sortBy]);
const parameterSpaceData: ParameterSpaceDataPoint[] = allTrials.map(trial => {
const params = Object.values(trial.design_variables);
return {
trial_number: trial.trial_number,
x: params[0] || 0,
y: params[1] || 0,
objective: trial.objective,
isBest: trial.objective === bestValue,
};
});
// Auto-refresh polling (every 3 seconds) for trial history
useEffect(() => {
if (!selectedStudyId) return;
const refreshInterval = setInterval(() => {
apiClient.getStudyHistory(selectedStudyId)
.then(data => {
const validTrials = data.trials.filter(t => t.objective !== null && t.objective !== undefined);
setAllTrials(validTrials);
if (validTrials.length > 0) {
const minObj = Math.min(...validTrials.map(t => t.objective));
setBestValue(minObj);
}
})
.catch(err => console.error('Auto-refresh failed:', err));
}, 3000); // Poll every 3 seconds
return () => clearInterval(refreshInterval);
}, [selectedStudyId]);
// Prepare chart data with proper null/undefined handling
const convergenceData: ConvergenceDataPoint[] = allTrials
.filter(t => t.objective !== null && t.objective !== undefined)
.sort((a, b) => a.trial_number - b.trial_number)
.map((trial, idx, arr) => {
const previousTrials = arr.slice(0, idx + 1);
const validObjectives = previousTrials.map(t => t.objective).filter(o => o !== null && o !== undefined);
return {
trial_number: trial.trial_number,
objective: trial.objective,
best_so_far: validObjectives.length > 0 ? Math.min(...validObjectives) : trial.objective,
};
});
const parameterSpaceData: ParameterSpaceDataPoint[] = allTrials
.filter(t => t.objective !== null && t.objective !== undefined && t.design_variables)
.map(trial => {
const params = Object.values(trial.design_variables);
return {
trial_number: trial.trial_number,
x: params[0] || 0,
y: params[1] || 0,
objective: trial.objective,
isBest: trial.objective === bestValue,
};
});
// Calculate average objective
const avgObjective = allTrials.length > 0
? allTrials.reduce((sum, t) => sum + t.objective, 0) / allTrials.length
const validObjectives = allTrials.filter(t => t.objective !== null && t.objective !== undefined).map(t => t.objective);
const avgObjective = validObjectives.length > 0
? validObjectives.reduce((sum, obj) => sum + obj, 0) / validObjectives.length
: 0;
// Get parameter names
const paramNames = allTrials.length > 0 ? Object.keys(allTrials[0].design_variables) : [];
const paramNames = allTrials.length > 0 && allTrials[0].design_variables
? Object.keys(allTrials[0].design_variables)
: [];
// Helper: Format parameter label with unit from metadata
const getParamLabel = (paramName: string, index: number): string => {
if (!studyMetadata?.design_variables) {
return paramName || `Parameter ${index + 1}`;
}
const dv = studyMetadata.design_variables.find((v: any) => v.name === paramName);
if (dv && dv.unit) {
return `${paramName} (${dv.unit})`;
}
return paramName || `Parameter ${index + 1}`;
// Toggle trial expansion
const toggleTrialExpansion = (trialNumber: number) => {
setExpandedTrials(prev => {
const newSet = new Set(prev);
if (newSet.has(trialNumber)) {
newSet.delete(trialNumber);
} else {
newSet.add(trialNumber);
}
return newSet;
});
};
// Export functions
@@ -169,7 +235,7 @@ export default function Dashboard({ studies, selectedStudyId, onStudySelect }: D
const rows = allTrials.map(t => [
t.trial_number,
t.objective,
...paramNames.map(k => t.design_variables[k])
...paramNames.map(k => t.design_variables?.[k] ?? '')
].join(','));
const csv = [headers, ...rows].join('\n');
const blob = new Blob([csv], { type: 'text/csv' });
@@ -183,7 +249,7 @@ export default function Dashboard({ studies, selectedStudyId, onStudySelect }: D
};
return (
<div className="container mx-auto p-6">
<div className="container mx-auto">
{/* Alerts */}
<div className="fixed top-4 right-4 z-50 space-y-2">
{alerts.map(alert => (
@@ -201,12 +267,24 @@ export default function Dashboard({ studies, selectedStudyId, onStudySelect }: D
</div>
{/* Header */}
<header className="mb-8 flex items-center justify-between border-b border-dark-500 pb-4">
<header className="mb-8 flex items-center justify-between border-b border-dark-600 pb-4">
<div>
<h1 className="text-3xl font-bold text-primary-400">Atomizer Dashboard</h1>
<p className="text-dark-200 mt-2">Real-time optimization monitoring</p>
<h1 className="text-3xl font-bold text-primary-400">Live Dashboard</h1>
<p className="text-dark-300 mt-1">Real-time optimization monitoring</p>
</div>
<div className="flex gap-2">
<button
onClick={() => {
if (selectedStudyId) {
window.open(`http://localhost:8080?study=${selectedStudyId}`, '_blank');
}
}}
className="btn-secondary"
disabled={!selectedStudyId}
title="Open Optuna Dashboard (make sure it's running on port 8080)"
>
Optuna Dashboard
</button>
<button onClick={exportJSON} className="btn-secondary" disabled={allTrials.length === 0}>
Export JSON
</button>
@@ -226,7 +304,7 @@ export default function Dashboard({ studies, selectedStudyId, onStudySelect }: D
key={study.id}
study={study}
isActive={study.id === selectedStudyId}
onClick={() => onStudySelect(study.id)}
onClick={() => setSelectedStudyId(study.id)}
/>
))}
</div>
@@ -248,14 +326,6 @@ export default function Dashboard({ studies, selectedStudyId, onStudySelect }: D
value={avgObjective > 0 ? avgObjective.toFixed(4) : '-'}
valueColor="text-blue-400"
/>
<MetricCard
label="Connection"
value={isConnected ? 'Connected' : 'Disconnected'}
valueColor={isConnected ? 'text-green-400' : 'text-red-400'}
/>
</div>
<div className="grid grid-cols-4 gap-4 mb-6">
<MetricCard
label="Pruned"
value={prunedCount}
@@ -264,25 +334,38 @@ export default function Dashboard({ studies, selectedStudyId, onStudySelect }: D
</div>
{/* Protocol 13: Intelligent Optimizer & Pareto Front */}
{selectedStudyId && (
{selectedStudyId && paretoFront.length > 0 && studyMetadata && studyMetadata.objectives && (
<div className="grid grid-cols-2 gap-6 mb-6">
<OptimizerPanel studyId={selectedStudyId} />
{paretoFront.length > 0 && studyMetadata && (
<ParetoPlot
paretoData={paretoFront}
objectives={studyMetadata.objectives || []}
/>
)}
<Card title="Optimizer Strategy">
<div className="space-y-2">
<div className="text-sm text-dark-300">
<span className="font-semibold text-dark-100">Algorithm:</span> {studyMetadata.sampler || 'NSGA-II'}
</div>
<div className="text-sm text-dark-300">
<span className="font-semibold text-dark-100">Type:</span> Multi-objective
</div>
<div className="text-sm text-dark-300">
<span className="font-semibold text-dark-100">Objectives:</span> {studyMetadata.objectives?.length || 2}
</div>
<div className="text-sm text-dark-300">
<span className="font-semibold text-dark-100">Design Variables:</span> {studyMetadata.design_variables?.length || 0}
</div>
</div>
</Card>
<ParetoPlot
paretoData={paretoFront}
objectives={studyMetadata.objectives}
/>
</div>
)}
{/* Parallel Coordinates (full width for multi-objective) */}
{paretoFront.length > 0 && studyMetadata && (
{paretoFront.length > 0 && studyMetadata && studyMetadata.objectives && studyMetadata.design_variables && (
<div className="mb-6">
<ParallelCoordinatesPlot
paretoData={paretoFront}
objectives={studyMetadata.objectives || []}
designVariables={studyMetadata.design_variables || []}
objectives={studyMetadata.objectives}
designVariables={studyMetadata.design_variables}
/>
</div>
)}
@@ -344,14 +427,14 @@ export default function Dashboard({ studies, selectedStudyId, onStudySelect }: D
dataKey="x"
stroke="#94a3b8"
name={paramNames[0] || 'X'}
label={{ value: getParamLabel(paramNames[0], 0), position: 'insideBottom', offset: -5, fill: '#94a3b8' }}
label={{ value: paramNames[0] || 'Parameter 1', position: 'insideBottom', offset: -5, fill: '#94a3b8' }}
/>
<YAxis
type="number"
dataKey="y"
stroke="#94a3b8"
name={paramNames[1] || 'Y'}
label={{ value: getParamLabel(paramNames[1], 1), angle: -90, position: 'insideLeft', fill: '#94a3b8' }}
label={{ value: paramNames[1] || 'Parameter 2', angle: -90, position: 'insideLeft', fill: '#94a3b8' }}
/>
<Tooltip
cursor={{ strokeDasharray: '3 3' }}
@@ -381,38 +464,149 @@ export default function Dashboard({ studies, selectedStudyId, onStudySelect }: D
</Card>
</div>
{/* Trial Feed */}
<Card title="Recent Trials">
<div className="space-y-2 max-h-96 overflow-y-auto">
{trials.length > 0 ? (
trials.map(trial => (
<div
key={trial.trial_number}
className={`p-3 rounded-lg transition-all duration-200 ${
trial.objective === bestValue
? 'bg-green-900 border-l-4 border-green-400'
: 'bg-dark-500 hover:bg-dark-400'
{/* Trial History with Sort Controls */}
<Card
title={
<div className="flex items-center justify-between w-full">
<span>Trial History ({displayedTrials.length} trials)</span>
<div className="flex gap-2">
<button
onClick={() => setSortBy('performance')}
className={`px-3 py-1 rounded text-sm ${
sortBy === 'performance'
? 'bg-primary-500 text-white'
: 'bg-dark-500 text-dark-200 hover:bg-dark-400'
}`}
>
<div className="flex justify-between items-center mb-1">
<span className="font-semibold text-primary-400">
Trial #{trial.trial_number}
</span>
<span className={`font-mono text-lg ${
trial.objective === bestValue ? 'text-green-400 font-bold' : 'text-dark-100'
}`}>
{trial.objective.toFixed(4)}
</span>
Best First
</button>
<button
onClick={() => setSortBy('chronological')}
className={`px-3 py-1 rounded text-sm ${
sortBy === 'chronological'
? 'bg-primary-500 text-white'
: 'bg-dark-500 text-dark-200 hover:bg-dark-400'
}`}
>
Newest First
</button>
</div>
</div>
}
>
<div className="space-y-2 max-h-[600px] overflow-y-auto">
{displayedTrials.length > 0 ? (
displayedTrials.map(trial => {
const isExpanded = expandedTrials.has(trial.trial_number);
const isBest = trial.objective === bestValue;
return (
<div
key={trial.trial_number}
className={`rounded-lg transition-all duration-200 cursor-pointer ${
isBest
? 'bg-green-900 border-l-4 border-green-400'
: 'bg-dark-500 hover:bg-dark-400'
}`}
onClick={() => toggleTrialExpansion(trial.trial_number)}
>
{/* Collapsed View */}
<div className="p-3">
<div className="flex justify-between items-center">
<span className="font-semibold text-primary-400">
Trial #{trial.trial_number}
{isBest && <span className="ml-2 text-xs bg-green-700 text-green-100 px-2 py-1 rounded">BEST</span>}
</span>
<div className="flex items-center gap-3">
<span className={`font-mono text-lg ${
isBest ? 'text-green-400 font-bold' : 'text-dark-100'
}`}>
{trial.objective !== null && trial.objective !== undefined
? trial.objective.toFixed(4)
: 'N/A'}
</span>
<span className="text-dark-400 text-sm">
{isExpanded ? '▼' : '▶'}
</span>
</div>
</div>
{/* Quick Preview */}
{!isExpanded && trial.results && Object.keys(trial.results).length > 0 && (
<div className="text-xs text-primary-300 flex flex-wrap gap-3 mt-2">
{trial.results.mass && (
<span>Mass: {trial.results.mass.toFixed(2)}g</span>
)}
{trial.results.frequency && (
<span>Freq: {trial.results.frequency.toFixed(2)}Hz</span>
)}
</div>
)}
</div>
{/* Expanded View */}
{isExpanded && (
<div className="px-3 pb-3 space-y-3">
{/* Design Variables */}
{trial.design_variables && Object.keys(trial.design_variables).length > 0 && (
<div className="border-t border-dark-400 pt-3">
<h4 className="text-sm font-semibold text-dark-200 mb-2">Design Variables</h4>
<div className="grid grid-cols-2 gap-2 text-xs">
{Object.entries(trial.design_variables).map(([key, val]) => (
<div key={key} className="flex justify-between bg-dark-600 px-2 py-1 rounded">
<span className="text-dark-300">{key}:</span>
<span className="text-dark-100 font-mono">{val.toFixed(4)}</span>
</div>
))}
</div>
</div>
)}
{/* Results */}
{trial.results && Object.keys(trial.results).length > 0 && (
<div className="border-t border-dark-400 pt-3">
<h4 className="text-sm font-semibold text-dark-200 mb-2">Extracted Results</h4>
<div className="grid grid-cols-2 gap-2 text-xs">
{Object.entries(trial.results).map(([key, val]) => (
<div key={key} className="flex justify-between bg-dark-600 px-2 py-1 rounded">
<span className="text-dark-300">{key}:</span>
<span className="text-primary-300 font-mono">
{typeof val === 'number' ? val.toFixed(4) : String(val)}
</span>
</div>
))}
</div>
</div>
)}
{/* All User Attributes */}
{trial.user_attrs && Object.keys(trial.user_attrs).length > 0 && (
<div className="border-t border-dark-400 pt-3">
<h4 className="text-sm font-semibold text-dark-200 mb-2">All Attributes</h4>
<div className="max-h-48 overflow-y-auto">
<pre className="text-xs text-dark-300 bg-dark-700 p-2 rounded">
{JSON.stringify(trial.user_attrs, null, 2)}
</pre>
</div>
</div>
)}
{/* Timestamps */}
{trial.start_time && trial.end_time && (
<div className="border-t border-dark-400 pt-3 text-xs text-dark-400">
<div className="flex justify-between">
<span>Duration:</span>
<span>
{((new Date(trial.end_time).getTime() - new Date(trial.start_time).getTime()) / 1000).toFixed(1)}s
</span>
</div>
</div>
)}
</div>
)}
</div>
<div className="text-xs text-dark-200 flex flex-wrap gap-3">
{Object.entries(trial.design_variables).map(([key, val]) => (
<span key={key}>
<span className="text-dark-400">{key}:</span> {val.toFixed(3)}
</span>
))}
</div>
</div>
))
);
})
) : (
<div className="text-center py-8 text-dark-300">
No trials yet. Waiting for optimization to start...