Files
Atomizer/atomizer-dashboard/frontend/src/components/tracker/OptimizerStatePanel.tsx
Anto01 73a7b9d9f1 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>
2026-01-13 15:53:55 -05:00

272 lines
9.7 KiB
TypeScript

import { useEffect, useState } from 'react';
import { Cpu, Layers, Target, Database, Brain, RefreshCw } from 'lucide-react';
import { apiClient } from '../../api/client';
interface OptimizerState {
available: boolean;
source?: string;
phase?: string;
phase_description?: string;
phase_progress?: number;
current_strategy?: string;
sampler?: {
name: string;
description: string;
};
objectives?: Array<{
name: string;
direction: string;
current_best?: number;
unit?: string;
}>;
plan?: {
total_phases: number;
current_phase: number;
phases: string[];
};
completed_trials?: number;
total_trials?: number;
}
interface OptimizerStatePanelProps {
studyId?: string;
// Legacy props for backwards compatibility
sampler?: string;
nTrials?: number;
completedTrials?: number;
feaTrials?: number;
nnTrials?: number;
objectives?: Array<{ name: string; direction: string }>;
isMultiObjective?: boolean;
paretoSize?: number;
}
export function OptimizerStatePanel({
studyId,
sampler: legacySampler = 'TPESampler',
nTrials: legacyNTrials = 100,
completedTrials: legacyCompletedTrials = 0,
feaTrials = 0,
nnTrials = 0,
objectives: legacyObjectives = [],
isMultiObjective = false,
paretoSize = 0
}: OptimizerStatePanelProps) {
const [state, setState] = useState<OptimizerState | null>(null);
const [loading, setLoading] = useState(false);
// Fetch dynamic state if studyId is provided
useEffect(() => {
if (!studyId) return;
const fetchState = async () => {
setLoading(true);
try {
const result = await apiClient.getOptimizerState(studyId);
setState(result);
} catch {
setState(null);
} finally {
setLoading(false);
}
};
fetchState();
const interval = setInterval(fetchState, 5000); // Refresh every 5 seconds
return () => clearInterval(interval);
}, [studyId]);
// Use dynamic state if available, otherwise fall back to legacy props
const sampler = state?.sampler?.name || legacySampler;
const samplerDescription = state?.sampler?.description;
const nTrials = state?.total_trials || legacyNTrials;
const completedTrials = state?.completed_trials ?? legacyCompletedTrials;
const objectives = state?.objectives?.map(o => ({ name: o.name, direction: o.direction })) || legacyObjectives;
const phase = state?.phase || getPhase(completedTrials, nTrials);
const phaseDescription = state?.phase_description;
const phaseProgress = state?.phase_progress ?? getPhaseProgress(completedTrials, nTrials);
const plan = state?.plan;
// Determine optimizer phase based on progress (fallback)
function getPhase(completed: number, total: number): string {
if (completed === 0) return 'initializing';
if (completed < 10) return 'exploration';
if (completed < total * 0.5) return 'exploitation';
if (completed < total * 0.9) return 'refinement';
return 'convergence';
}
function getPhaseProgress(completed: number, total: number): number {
if (completed === 0) return 0;
if (completed < 10) return completed / 10;
if (completed < total * 0.5) return (completed - 10) / (total * 0.5 - 10);
if (completed < total * 0.9) return (completed - total * 0.5) / (total * 0.4);
return (completed - total * 0.9) / (total * 0.1);
}
// Format sampler name for display
const formatSampler = (s: string) => {
const samplers: Record<string, string> = {
'TPESampler': 'TPE (Bayesian)',
'NSGAIISampler': 'NSGA-II',
'NSGAIIISampler': 'NSGA-III',
'CmaEsSampler': 'CMA-ES',
'RandomSampler': 'Random',
'GridSampler': 'Grid',
'QMCSampler': 'Quasi-Monte Carlo'
};
return samplers[s] || s;
};
const phaseColors: Record<string, string> = {
'initializing': 'text-dark-400',
'exploration': 'text-primary-400',
'exploitation': 'text-yellow-400',
'refinement': 'text-blue-400',
'convergence': 'text-green-400'
};
const phaseLabels: Record<string, string> = {
'initializing': 'Initializing',
'exploration': 'Exploration',
'exploitation': 'Exploitation',
'refinement': 'Refinement',
'convergence': 'Convergence'
};
return (
<div className="bg-dark-750 rounded-lg border border-dark-600 p-4">
{/* Header */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Cpu className="w-5 h-5 text-primary-400" />
<span className="font-semibold text-white">Optimizer State</span>
</div>
{loading && <RefreshCw className="w-4 h-4 text-dark-400 animate-spin" />}
</div>
{/* Sampler with description */}
<div className="bg-dark-700 rounded-lg p-3 mb-3">
<div className="flex items-center gap-2 mb-1">
<Target className="w-4 h-4 text-primary-400" />
<span className="font-medium text-white">{formatSampler(sampler)}</span>
</div>
{samplerDescription && (
<p className="text-xs text-dark-400 mt-1">{samplerDescription}</p>
)}
</div>
{/* Phase progress */}
<div className="bg-dark-700 rounded-lg p-3 mb-3">
<div className="flex justify-between mb-2">
<span className="text-dark-300 text-sm">
Phase {plan ? `${plan.current_phase}/${plan.total_phases}` : ''}
</span>
<span className={`font-medium ${phaseColors[phase] || 'text-primary-400'}`}>
{phaseLabels[phase] || phase}
</span>
</div>
<div className="h-2 bg-dark-600 rounded-full overflow-hidden">
<div
className="h-full bg-primary-500 transition-all duration-500"
style={{ width: `${Math.min(100, phaseProgress * 100)}%` }}
/>
</div>
{phaseDescription && (
<p className="text-xs text-dark-400 mt-2">{phaseDescription}</p>
)}
</div>
{/* FEA vs NN Trials (for hybrid optimizations) */}
{(feaTrials > 0 || nnTrials > 0) && (
<div className="mb-3">
<div className="text-xs text-dark-400 uppercase mb-2">Trial Sources</div>
<div className="flex gap-2">
<div className="flex-1 bg-dark-700 rounded-lg p-2 text-center">
<Database className="w-4 h-4 text-blue-400 mx-auto mb-1" />
<div className="text-lg font-bold text-blue-400">{feaTrials}</div>
<div className="text-xs text-dark-400">FEA</div>
</div>
<div className="flex-1 bg-dark-700 rounded-lg p-2 text-center">
<Brain className="w-4 h-4 text-purple-400 mx-auto mb-1" />
<div className="text-lg font-bold text-purple-400">{nnTrials}</div>
<div className="text-xs text-dark-400">Neural Net</div>
</div>
</div>
{nnTrials > 0 && (
<div className="mt-2 text-xs text-dark-400 text-center">
{((nnTrials / (feaTrials + nnTrials)) * 100).toFixed(0)}% acceleration from surrogate
</div>
)}
</div>
)}
{/* Live objectives with current best */}
{objectives.length > 0 && (
<div className="mb-3">
<div className="flex items-center gap-2 mb-2">
<Layers className="w-4 h-4 text-dark-400" />
<span className="text-xs text-dark-400 uppercase">
{isMultiObjective || objectives.length > 1 ? 'Multi-Objective' : 'Single Objective'}
</span>
</div>
<div className="space-y-1">
{objectives.slice(0, 3).map((obj, idx) => {
const stateObj = state?.objectives?.[idx];
return (
<div
key={idx}
className="flex items-center justify-between text-sm bg-dark-700 rounded px-2 py-1"
>
<span className="text-dark-300 truncate" title={obj.name}>
{obj.name.length > 15 ? obj.name.slice(0, 13) + '...' : obj.name}
</span>
<div className="flex items-center gap-2">
{stateObj?.current_best !== undefined && stateObj.current_best !== null && (
<span className="font-mono text-primary-400 text-xs">
{stateObj.current_best.toFixed(2)}
{stateObj.unit && <span className="text-dark-500 ml-0.5">{stateObj.unit}</span>}
</span>
)}
<span className={`text-xs px-1.5 py-0.5 rounded ${
obj.direction === 'minimize' ? 'bg-green-900/50 text-green-400' : 'bg-blue-900/50 text-blue-400'
}`}>
{obj.direction === 'minimize' ? 'min' : 'max'}
</span>
</div>
</div>
);
})}
{objectives.length > 3 && (
<div className="text-xs text-dark-500 text-center">
+{objectives.length - 3} more
</div>
)}
</div>
</div>
)}
{/* Pareto Front Size (for multi-objective) */}
{(isMultiObjective || objectives.length > 1) && paretoSize > 0 && (
<div className="pt-3 border-t border-dark-600">
<div className="flex items-center justify-between">
<span className="text-xs text-dark-400">Pareto Front Size</span>
<span className="text-sm font-medium text-primary-400">
{paretoSize} solutions
</span>
</div>
</div>
)}
{/* Source indicator (for debugging) */}
{state?.source && (
<div className="mt-3 pt-2 border-t border-dark-700">
<span className="text-xs text-dark-500">
Source: {state.source}
</span>
</div>
)}
</div>
);
}