feat: Major update with validators, skills, dashboard, and docs reorganization
- Add validation framework (config, model, results, study validators) - Add Claude Code skills (create-study, run-optimization, generate-report, troubleshoot, analyze-model) - Add Atomizer Dashboard (React frontend + FastAPI backend) - Reorganize docs into structured directories (00-09) - Add neural surrogate modules and training infrastructure - Add multi-objective optimization support 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
27
atomizer-dashboard/frontend/src/App.tsx
Normal file
27
atomizer-dashboard/frontend/src/App.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { MainLayout } from './components/layout/MainLayout';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import Configurator from './pages/Configurator';
|
||||
import Results from './pages/Results';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<MainLayout />}>
|
||||
<Route index element={<Navigate to="/dashboard" replace />} />
|
||||
<Route path="dashboard" element={<Dashboard />} />
|
||||
<Route path="configurator" element={<Configurator />} />
|
||||
<Route path="results" element={<Results />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
58
atomizer-dashboard/frontend/src/api/client.ts
Normal file
58
atomizer-dashboard/frontend/src/api/client.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Study, StudyListResponse, HistoryResponse, PruningResponse, StudyStatus } from '../types';
|
||||
|
||||
const API_BASE = '/api';
|
||||
|
||||
class ApiClient {
|
||||
async getStudies(): Promise<StudyListResponse> {
|
||||
const response = await fetch(`${API_BASE}/optimization/studies`);
|
||||
if (!response.ok) throw new Error('Failed to fetch studies');
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async getStudyStatus(studyId: string): Promise<StudyStatus> {
|
||||
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/status`);
|
||||
if (!response.ok) throw new Error('Failed to fetch study status');
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async getStudyHistory(studyId: string): Promise<HistoryResponse> {
|
||||
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/history`);
|
||||
if (!response.ok) throw new Error('Failed to fetch study history');
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async getStudyPruning(studyId: string): Promise<PruningResponse> {
|
||||
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/pruning`);
|
||||
if (!response.ok) throw new Error('Failed to fetch pruning data');
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async createStudy(config: any): Promise<{ study_id: string }> {
|
||||
const response = await fetch(`${API_BASE}/optimization/studies`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(config),
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to create study');
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async getStudyReport(studyId: string): Promise<{ content: string }> {
|
||||
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/report`);
|
||||
if (!response.ok) throw new Error('Failed to fetch report');
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// Future endpoints for control
|
||||
async startOptimization(studyId: string): Promise<void> {
|
||||
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/start`, { method: 'POST' });
|
||||
if (!response.ok) throw new Error('Failed to start optimization');
|
||||
}
|
||||
|
||||
async stopOptimization(studyId: string): Promise<void> {
|
||||
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/stop`, { method: 'POST' });
|
||||
if (!response.ok) throw new Error('Failed to stop optimization');
|
||||
}
|
||||
}
|
||||
|
||||
export const apiClient = new ApiClient();
|
||||
24
atomizer-dashboard/frontend/src/components/Badge.tsx
Normal file
24
atomizer-dashboard/frontend/src/components/Badge.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
|
||||
type BadgeVariant = 'success' | 'warning' | 'error' | 'info';
|
||||
|
||||
interface BadgeProps {
|
||||
children: React.ReactNode;
|
||||
variant?: BadgeVariant;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const variantClasses: Record<BadgeVariant, string> = {
|
||||
success: 'badge-success',
|
||||
warning: 'badge-warning',
|
||||
error: 'badge-error',
|
||||
info: 'badge-info',
|
||||
};
|
||||
|
||||
export function Badge({ children, variant = 'info', className = '' }: BadgeProps) {
|
||||
return (
|
||||
<span className={`${variantClasses[variant]} ${className}`}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
16
atomizer-dashboard/frontend/src/components/Card.tsx
Normal file
16
atomizer-dashboard/frontend/src/components/Card.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
|
||||
interface CardProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
title?: string | React.ReactNode;
|
||||
}
|
||||
|
||||
export function Card({ children, className = '', title }: CardProps) {
|
||||
return (
|
||||
<div className={`card ${className}`}>
|
||||
{title && <h2 className="text-xl font-bold mb-4 text-primary-400">{title}</h2>}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
17
atomizer-dashboard/frontend/src/components/MetricCard.tsx
Normal file
17
atomizer-dashboard/frontend/src/components/MetricCard.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
|
||||
interface MetricCardProps {
|
||||
label: string;
|
||||
value: string | number;
|
||||
className?: string;
|
||||
valueColor?: string;
|
||||
}
|
||||
|
||||
export function MetricCard({ label, value, className = '', valueColor = 'text-primary-400' }: MetricCardProps) {
|
||||
return (
|
||||
<div className={`bg-dark-500 rounded-lg p-4 ${className}`}>
|
||||
<div className="text-sm text-dark-200 mb-1">{label}</div>
|
||||
<div className={`text-2xl font-bold ${valueColor}`}>{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -39,17 +39,22 @@ interface ParallelCoordinatesPlotProps {
|
||||
objectives: Objective[];
|
||||
designVariables: DesignVariable[];
|
||||
constraints?: Constraint[];
|
||||
paretoFront?: ParetoTrial[];
|
||||
}
|
||||
|
||||
export function ParallelCoordinatesPlot({
|
||||
paretoData,
|
||||
objectives,
|
||||
designVariables,
|
||||
constraints = []
|
||||
constraints = [],
|
||||
paretoFront = []
|
||||
}: ParallelCoordinatesPlotProps) {
|
||||
const [hoveredTrial, setHoveredTrial] = useState<number | null>(null);
|
||||
const [selectedTrials, setSelectedTrials] = useState<Set<number>>(new Set());
|
||||
|
||||
// Create set of Pareto front trial numbers for easy lookup
|
||||
const paretoTrialNumbers = new Set(paretoFront.map(t => t.trial_number));
|
||||
|
||||
// Safety checks
|
||||
if (!paretoData || paretoData.length === 0) {
|
||||
return (
|
||||
@@ -83,9 +88,10 @@ export function ParallelCoordinatesPlot({
|
||||
|
||||
// Add design variables
|
||||
designVariables.forEach(dv => {
|
||||
const paramName = dv.parameter || dv.name; // Support both formats
|
||||
axes.push({
|
||||
name: dv.name,
|
||||
label: dv.unit ? `${dv.name}\n(${dv.unit})` : dv.name,
|
||||
name: paramName,
|
||||
label: dv.unit ? `${paramName}\n(${dv.unit})` : paramName,
|
||||
type: 'design_var',
|
||||
unit: dv.unit
|
||||
});
|
||||
@@ -134,9 +140,10 @@ export function ParallelCoordinatesPlot({
|
||||
const trialData = paretoData.map(trial => {
|
||||
const values: number[] = [];
|
||||
|
||||
// Design variables
|
||||
// Design variables - use .parameter field from metadata
|
||||
designVariables.forEach(dv => {
|
||||
values.push(trial.params[dv.name] ?? 0);
|
||||
const paramName = dv.parameter || dv.name; // Support both formats
|
||||
values.push(trial.params[paramName] ?? 0);
|
||||
});
|
||||
|
||||
// Objectives
|
||||
@@ -152,10 +159,32 @@ export function ParallelCoordinatesPlot({
|
||||
return {
|
||||
trial_number: trial.trial_number,
|
||||
values,
|
||||
feasible: trial.constraint_satisfied !== false
|
||||
feasible: trial.constraint_satisfied !== false,
|
||||
objectiveValues: trial.values || []
|
||||
};
|
||||
});
|
||||
|
||||
// Rank trials by their first objective (for multi-objective, this is just one metric)
|
||||
// For proper multi-objective ranking, we use Pareto dominance
|
||||
const rankedTrials = [...trialData].sort((a, b) => {
|
||||
// Primary: Pareto front members come first
|
||||
const aIsPareto = paretoTrialNumbers.has(a.trial_number);
|
||||
const bIsPareto = paretoTrialNumbers.has(b.trial_number);
|
||||
if (aIsPareto && !bIsPareto) return -1;
|
||||
if (!aIsPareto && bIsPareto) return 1;
|
||||
|
||||
// Secondary: Sort by first objective value (minimize assumed)
|
||||
const aObj = a.objectiveValues[0] ?? Infinity;
|
||||
const bObj = b.objectiveValues[0] ?? Infinity;
|
||||
return aObj - bObj;
|
||||
});
|
||||
|
||||
// Create ranking map: trial_number -> rank (0-indexed)
|
||||
const trialRanks = new Map<number, number>();
|
||||
rankedTrials.forEach((trial, index) => {
|
||||
trialRanks.set(trial.trial_number, index);
|
||||
});
|
||||
|
||||
// Calculate min/max for normalization
|
||||
const ranges = axes.map((_, axisIdx) => {
|
||||
const values = trialData.map(d => d.values[axisIdx]);
|
||||
@@ -192,12 +221,26 @@ export function ParallelCoordinatesPlot({
|
||||
setSelectedTrials(newSelected);
|
||||
};
|
||||
|
||||
// Color scheme - highly visible
|
||||
// Color scheme - gradient grayscale for top 10, light gray for rest
|
||||
const getLineColor = (trial: typeof trialData[0], isHovered: boolean, isSelected: boolean) => {
|
||||
if (isSelected) return '#FF6B00'; // Bright orange for selected
|
||||
if (!trial.feasible) return '#DC2626'; // Red for infeasible
|
||||
if (isHovered) return '#2563EB'; // Blue for hover
|
||||
return '#10B981'; // Green for feasible
|
||||
if (!trial.feasible) return '#DC2626'; // Red for infeasible
|
||||
|
||||
const rank = trialRanks.get(trial.trial_number) ?? 999;
|
||||
|
||||
// Top 10: Gradient from dark gray (#374151) to light gray (#9CA3AF)
|
||||
if (rank < 10) {
|
||||
// Interpolate: rank 0 = darkest, rank 9 = lighter
|
||||
const t = rank / 9; // 0 to 1
|
||||
const r = Math.round(55 + t * (156 - 55)); // 55 to 156
|
||||
const g = Math.round(65 + t * (163 - 65)); // 65 to 163
|
||||
const b = Math.round(81 + t * (175 - 81)); // 81 to 175
|
||||
return `rgb(${r}, ${g}, ${b})`;
|
||||
}
|
||||
|
||||
// Remaining trials: Very light gray
|
||||
return '#D1D5DB'; // Very light gray
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -371,8 +414,12 @@ export function ParallelCoordinatesPlot({
|
||||
{/* Legend */}
|
||||
<div className="flex gap-8 justify-center mt-6 text-sm border-t border-gray-200 pt-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-10 h-1" style={{ backgroundColor: '#10B981' }} />
|
||||
<span className="text-gray-700 font-medium">Feasible</span>
|
||||
<div className="w-10 h-1" style={{ background: 'linear-gradient(to right, #374151, #9CA3AF)' }} />
|
||||
<span className="text-gray-700 font-medium">Top 10 (gradient)</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-10 h-1" style={{ backgroundColor: '#D1D5DB' }} />
|
||||
<span className="text-gray-700 font-medium">Others</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-10 h-1" style={{ backgroundColor: '#DC2626' }} />
|
||||
|
||||
@@ -22,14 +22,18 @@ interface Objective {
|
||||
interface ParetoPlotProps {
|
||||
paretoData: ParetoTrial[];
|
||||
objectives: Objective[];
|
||||
allTrials?: ParetoTrial[]; // All trials including non-Pareto
|
||||
}
|
||||
|
||||
type NormalizationMode = 'raw' | 'minmax' | 'zscore';
|
||||
|
||||
export function ParetoPlot({ paretoData, objectives }: ParetoPlotProps) {
|
||||
export function ParetoPlot({ paretoData, objectives, allTrials }: ParetoPlotProps) {
|
||||
const [normMode, setNormMode] = useState<NormalizationMode>('raw');
|
||||
|
||||
if (paretoData.length === 0) {
|
||||
// Use allTrials if provided, otherwise fall back to paretoData
|
||||
const trialsToShow = allTrials && allTrials.length > 0 ? allTrials : paretoData;
|
||||
|
||||
if (trialsToShow.length === 0) {
|
||||
return (
|
||||
<div className="bg-dark-700 rounded-lg p-6 border border-dark-600">
|
||||
<h3 className="text-lg font-semibold mb-4 text-dark-100">Pareto Front</h3>
|
||||
@@ -40,12 +44,16 @@ export function ParetoPlot({ paretoData, objectives }: ParetoPlotProps) {
|
||||
);
|
||||
}
|
||||
|
||||
// Extract raw values
|
||||
const rawData = paretoData.map(trial => ({
|
||||
// Create set of Pareto front trial numbers for easy lookup
|
||||
const paretoTrialNumbers = new Set(paretoData.map(t => t.trial_number));
|
||||
|
||||
// Extract raw values for ALL trials
|
||||
const rawData = trialsToShow.map(trial => ({
|
||||
x: trial.values[0],
|
||||
y: trial.values[1],
|
||||
trial_number: trial.trial_number,
|
||||
feasible: trial.constraint_satisfied !== false
|
||||
feasible: trial.constraint_satisfied !== false,
|
||||
isPareto: paretoTrialNumbers.has(trial.trial_number)
|
||||
}));
|
||||
|
||||
// Calculate statistics for normalization
|
||||
@@ -89,11 +97,12 @@ export function ParetoPlot({ paretoData, objectives }: ParetoPlotProps) {
|
||||
rawX: d.x,
|
||||
rawY: d.y,
|
||||
trial_number: d.trial_number,
|
||||
feasible: d.feasible
|
||||
feasible: d.feasible,
|
||||
isPareto: d.isPareto
|
||||
}));
|
||||
|
||||
// Sort data by x-coordinate for Pareto front line
|
||||
const sortedData = [...data].sort((a, b) => a.x - b.x);
|
||||
// Sort ONLY Pareto front data by x-coordinate for line
|
||||
const paretoOnlyData = data.filter(d => d.isPareto).sort((a, b) => a.x - b.x);
|
||||
|
||||
// Get objective labels with normalization indicator
|
||||
const normSuffix = normMode === 'minmax' ? ' [0-1]' : normMode === 'zscore' ? ' [z-score]' : '';
|
||||
@@ -219,24 +228,29 @@ export function ParetoPlot({ paretoData, objectives }: ParetoPlotProps) {
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
{/* Pareto front line */}
|
||||
{/* Pareto front line - only connects Pareto front points */}
|
||||
<Line
|
||||
type="monotone"
|
||||
data={sortedData}
|
||||
data={paretoOnlyData}
|
||||
dataKey="y"
|
||||
stroke="#8b5cf6"
|
||||
strokeWidth={2}
|
||||
strokeWidth={3}
|
||||
dot={false}
|
||||
connectNulls={false}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<Scatter name="Pareto Front" data={data}>
|
||||
{/* All trials as scatter points */}
|
||||
<Scatter name="All Trials" data={data}>
|
||||
{data.map((entry, index) => (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
fill={entry.feasible ? '#10b981' : '#ef4444'}
|
||||
r={entry.feasible ? 6 : 4}
|
||||
opacity={entry.feasible ? 1 : 0.6}
|
||||
fill={
|
||||
entry.isPareto
|
||||
? (entry.feasible ? '#10b981' : '#ef4444') // Pareto: green/red
|
||||
: (entry.feasible ? '#64748b' : '#94a3b8') // Non-Pareto: gray tones
|
||||
}
|
||||
r={entry.isPareto ? 7 : 4}
|
||||
opacity={entry.isPareto ? 1 : 0.4}
|
||||
/>
|
||||
))}
|
||||
</Scatter>
|
||||
|
||||
53
atomizer-dashboard/frontend/src/components/StudyCard.tsx
Normal file
53
atomizer-dashboard/frontend/src/components/StudyCard.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import type { Study } from '../types';
|
||||
import { Badge } from './Badge';
|
||||
|
||||
interface StudyCardProps {
|
||||
study: Study;
|
||||
isActive: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export function StudyCard({ study, isActive, onClick }: StudyCardProps) {
|
||||
const percentage = study.progress.total > 0
|
||||
? (study.progress.current / study.progress.total) * 100
|
||||
: 0;
|
||||
|
||||
const statusVariant = study.status === 'completed'
|
||||
? 'success'
|
||||
: study.status === 'running'
|
||||
? 'info'
|
||||
: 'warning';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`p-4 rounded-lg cursor-pointer transition-all duration-200 ${
|
||||
isActive
|
||||
? 'bg-primary-900 border-l-4 border-primary-400'
|
||||
: 'bg-dark-500 hover:bg-dark-400'
|
||||
}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<h3 className="font-semibold text-dark-50 text-sm">{study.name}</h3>
|
||||
<Badge variant={statusVariant}>
|
||||
{study.status}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-dark-200 mb-2">
|
||||
{study.progress.current} / {study.progress.total} trials
|
||||
{study.best_value !== null && (
|
||||
<span className="ml-2">• Best: {study.best_value.toFixed(4)}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="w-full h-2 bg-dark-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-primary-600 to-primary-400 transition-all duration-300"
|
||||
style={{ width: `${percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
51
atomizer-dashboard/frontend/src/components/common/Button.tsx
Normal file
51
atomizer-dashboard/frontend/src/components/common/Button.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { ButtonHTMLAttributes, ReactNode } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
isLoading?: boolean;
|
||||
icon?: ReactNode;
|
||||
}
|
||||
|
||||
export const Button = ({
|
||||
children,
|
||||
className,
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
isLoading = false,
|
||||
icon,
|
||||
disabled,
|
||||
...props
|
||||
}: ButtonProps) => {
|
||||
const variants = {
|
||||
primary: 'bg-primary-600 hover:bg-primary-700 text-white shadow-sm',
|
||||
secondary: 'bg-dark-700 hover:bg-dark-600 text-dark-100 border border-dark-600',
|
||||
danger: 'bg-red-600 hover:bg-red-700 text-white',
|
||||
ghost: 'hover:bg-dark-700 text-dark-300 hover:text-white',
|
||||
};
|
||||
|
||||
const sizes = {
|
||||
sm: 'px-3 py-1.5 text-sm',
|
||||
md: 'px-4 py-2',
|
||||
lg: 'px-6 py-3 text-lg',
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
className={clsx(
|
||||
'inline-flex items-center justify-center rounded-lg font-medium transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
variants[variant],
|
||||
sizes[size],
|
||||
className
|
||||
)}
|
||||
disabled={disabled || isLoading}
|
||||
{...props}
|
||||
>
|
||||
{isLoading && <Loader2 className="w-4 h-4 mr-2 animate-spin" />}
|
||||
{!isLoading && icon && <span className="mr-2">{icon}</span>}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
21
atomizer-dashboard/frontend/src/components/common/Card.tsx
Normal file
21
atomizer-dashboard/frontend/src/components/common/Card.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { ReactNode } from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
interface CardProps {
|
||||
title?: string;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Card = ({ title, children, className }: CardProps) => {
|
||||
return (
|
||||
<div className={clsx('bg-dark-800 rounded-xl border border-dark-600 shadow-sm overflow-hidden', className)}>
|
||||
{title && (
|
||||
<div className="px-6 py-4 border-b border-dark-600">
|
||||
<h3 className="text-lg font-semibold text-white">{title}</h3>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-6">{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
39
atomizer-dashboard/frontend/src/components/common/Input.tsx
Normal file
39
atomizer-dashboard/frontend/src/components/common/Input.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { InputHTMLAttributes, forwardRef } from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
label?: string;
|
||||
error?: string;
|
||||
helperText?: string;
|
||||
}
|
||||
|
||||
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, label, error, helperText, ...props }, ref) => {
|
||||
return (
|
||||
<div className="w-full">
|
||||
{label && (
|
||||
<label className="block text-sm font-medium text-dark-200 mb-1.5">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<input
|
||||
ref={ref}
|
||||
className={clsx(
|
||||
'w-full bg-dark-800 border rounded-lg px-3 py-2 text-dark-50 placeholder-dark-400 focus:outline-none focus:ring-2 focus:ring-primary-500/50 transition-all duration-200',
|
||||
error
|
||||
? 'border-red-500 focus:border-red-500'
|
||||
: 'border-dark-600 focus:border-primary-500',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
{error && <p className="mt-1 text-sm text-red-400">{error}</p>}
|
||||
{helperText && !error && (
|
||||
<p className="mt-1 text-sm text-dark-400">{helperText}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Input.displayName = 'Input';
|
||||
@@ -0,0 +1,79 @@
|
||||
import { useRef, useState, Suspense } from 'react';
|
||||
import { Canvas, useFrame } from '@react-three/fiber';
|
||||
import { OrbitControls, Stage, useGLTF } from '@react-three/drei';
|
||||
import { Card } from '../common/Card';
|
||||
import { Button } from '../common/Button';
|
||||
import { Maximize2, RotateCcw } from 'lucide-react';
|
||||
|
||||
// Placeholder component for the mesh
|
||||
// In a real implementation, this would load the GLTF/OBJ file converted from Nastran
|
||||
const Model = ({ path }: { path?: string }) => {
|
||||
// For now, we'll render a simple box to demonstrate the viewer
|
||||
const meshRef = useRef<any>();
|
||||
|
||||
useFrame((state, delta) => {
|
||||
if (meshRef.current) {
|
||||
meshRef.current.rotation.y += delta * 0.2;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<mesh ref={meshRef}>
|
||||
<boxGeometry args={[2, 2, 2]} />
|
||||
<meshStandardMaterial color="#60a5fa" wireframe />
|
||||
</mesh>
|
||||
);
|
||||
};
|
||||
|
||||
interface MeshViewerProps {
|
||||
modelPath?: string;
|
||||
resultField?: string;
|
||||
}
|
||||
|
||||
export const MeshViewer = ({ modelPath, resultField }: MeshViewerProps) => {
|
||||
const [autoRotate, setAutoRotate] = useState(true);
|
||||
|
||||
return (
|
||||
<Card title="3D Result Viewer" className="h-full flex flex-col">
|
||||
<div className="relative flex-1 min-h-[400px] bg-dark-900 rounded-lg overflow-hidden border border-dark-700">
|
||||
<Canvas shadows dpr={[1, 2]} camera={{ fov: 50 }}>
|
||||
<Suspense fallback={null}>
|
||||
<Stage environment="city" intensity={0.6}>
|
||||
<Model path={modelPath} />
|
||||
</Stage>
|
||||
</Suspense>
|
||||
<OrbitControls autoRotate={autoRotate} />
|
||||
</Canvas>
|
||||
|
||||
{/* Controls Overlay */}
|
||||
<div className="absolute bottom-4 right-4 flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => setAutoRotate(!autoRotate)}
|
||||
icon={<RotateCcw className={`w-4 h-4 ${autoRotate ? 'animate-spin' : ''}`} />}
|
||||
>
|
||||
{autoRotate ? 'Stop Rotation' : 'Auto Rotate'}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
icon={<Maximize2 className="w-4 h-4" />}
|
||||
>
|
||||
Fullscreen
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Legend Overlay */}
|
||||
<div className="absolute top-4 left-4 bg-dark-800/80 p-3 rounded-lg backdrop-blur-sm border border-dark-600">
|
||||
<div className="text-xs font-medium text-dark-300 mb-2">Displacement (mm)</div>
|
||||
<div className="h-32 w-4 bg-gradient-to-t from-blue-500 via-green-500 to-red-500 rounded-full mx-auto" />
|
||||
<div className="flex justify-between text-[10px] text-dark-400 mt-1 w-12">
|
||||
<span>0.0</span>
|
||||
<span>5.2</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Card } from '../common/Card';
|
||||
import clsx from 'clsx';
|
||||
|
||||
interface MetricCardProps {
|
||||
label: string;
|
||||
value: string | number;
|
||||
valueColor?: string;
|
||||
subtext?: string;
|
||||
}
|
||||
|
||||
export const MetricCard = ({ label, value, valueColor = 'text-white', subtext }: MetricCardProps) => {
|
||||
return (
|
||||
<Card className="h-full">
|
||||
<div className="flex flex-col h-full justify-between">
|
||||
<span className="text-sm font-medium text-dark-300 uppercase tracking-wider">{label}</span>
|
||||
<div className="mt-2">
|
||||
<span className={clsx('text-3xl font-bold tracking-tight', valueColor)}>{value}</span>
|
||||
{subtext && <p className="text-xs text-dark-400 mt-1">{subtext}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,138 @@
|
||||
import { Card } from '../common/Card';
|
||||
|
||||
interface ParallelCoordinatesPlotProps {
|
||||
data: any[];
|
||||
dimensions: string[];
|
||||
colorBy?: string;
|
||||
}
|
||||
|
||||
export const ParallelCoordinatesPlot = ({ data, dimensions }: ParallelCoordinatesPlotProps) => {
|
||||
// Filter out null/undefined data points
|
||||
const validData = data.filter(d => d && dimensions.every(dim => d[dim] !== null && d[dim] !== undefined));
|
||||
|
||||
if (validData.length === 0 || dimensions.length === 0) {
|
||||
return (
|
||||
<Card title="Parallel Coordinates">
|
||||
<div className="h-80 flex items-center justify-center text-dark-300">
|
||||
No data available for parallel coordinates
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate min/max for each dimension for normalization
|
||||
const ranges = dimensions.map(dim => {
|
||||
const values = validData.map(d => d[dim]);
|
||||
return {
|
||||
min: Math.min(...values),
|
||||
max: Math.max(...values)
|
||||
};
|
||||
});
|
||||
|
||||
// Normalize function
|
||||
const normalize = (value: number, dimIdx: number): number => {
|
||||
const range = ranges[dimIdx];
|
||||
if (range.max === range.min) return 0.5;
|
||||
return (value - range.min) / (range.max - range.min);
|
||||
};
|
||||
|
||||
// Chart dimensions
|
||||
const width = 800;
|
||||
const height = 400;
|
||||
const margin = { top: 80, right: 20, bottom: 40, left: 20 };
|
||||
const plotWidth = width - margin.left - margin.right;
|
||||
const plotHeight = height - margin.top - margin.bottom;
|
||||
|
||||
const axisSpacing = plotWidth / (dimensions.length - 1);
|
||||
|
||||
return (
|
||||
<Card title={`Parallel Coordinates (${validData.length} solutions)`}>
|
||||
<svg width={width} height={height} className="overflow-visible">
|
||||
<g transform={`translate(${margin.left}, ${margin.top})`}>
|
||||
{/* Draw axes */}
|
||||
{dimensions.map((dim, i) => {
|
||||
const x = i * axisSpacing;
|
||||
return (
|
||||
<g key={dim} transform={`translate(${x}, 0)`}>
|
||||
{/* Axis line */}
|
||||
<line
|
||||
y1={0}
|
||||
y2={plotHeight}
|
||||
stroke="#475569"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
|
||||
{/* Axis label */}
|
||||
<text
|
||||
y={-10}
|
||||
textAnchor="middle"
|
||||
fill="#94a3b8"
|
||||
fontSize={12}
|
||||
className="select-none"
|
||||
transform={`rotate(-45, 0, -10)`}
|
||||
>
|
||||
{dim}
|
||||
</text>
|
||||
|
||||
{/* Min/max labels */}
|
||||
<text
|
||||
y={plotHeight + 15}
|
||||
textAnchor="middle"
|
||||
fill="#64748b"
|
||||
fontSize={10}
|
||||
>
|
||||
{ranges[i].min.toFixed(2)}
|
||||
</text>
|
||||
<text
|
||||
y={-25}
|
||||
textAnchor="middle"
|
||||
fill="#64748b"
|
||||
fontSize={10}
|
||||
>
|
||||
{ranges[i].max.toFixed(2)}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Draw lines for each trial */}
|
||||
{validData.map((trial, trialIdx) => {
|
||||
// Build path
|
||||
const pathData = dimensions.map((dim, i) => {
|
||||
const x = i * axisSpacing;
|
||||
const normalizedY = normalize(trial[dim], i);
|
||||
const y = plotHeight * (1 - normalizedY);
|
||||
return i === 0 ? `M ${x} ${y}` : `L ${x} ${y}`;
|
||||
}).join(' ');
|
||||
|
||||
return (
|
||||
<path
|
||||
key={trialIdx}
|
||||
d={pathData}
|
||||
fill="none"
|
||||
stroke={trial.isPareto !== false ? '#10b981' : '#60a5fa'}
|
||||
strokeWidth={1}
|
||||
opacity={0.4}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="transition-all duration-200"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
{/* Legend */}
|
||||
<div className="flex gap-6 justify-center mt-4 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-0.5 bg-green-400" />
|
||||
<span className="text-dark-200">Pareto Front</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-0.5 bg-blue-400" />
|
||||
<span className="text-dark-200">Other Solutions</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
import { ResponsiveContainer, ScatterChart, Scatter, XAxis, YAxis, ZAxis, Tooltip, Cell, CartesianGrid, Line } from 'recharts';
|
||||
import { Card } from '../common/Card';
|
||||
|
||||
interface ParetoPlotProps {
|
||||
data: any[];
|
||||
xKey: string;
|
||||
yKey: string;
|
||||
zKey?: string;
|
||||
}
|
||||
|
||||
export const ParetoPlot = ({ data, xKey, yKey, zKey }: ParetoPlotProps) => {
|
||||
// Filter out null/undefined data points
|
||||
const validData = data.filter(d =>
|
||||
d &&
|
||||
d[xKey] !== null && d[xKey] !== undefined &&
|
||||
d[yKey] !== null && d[yKey] !== undefined
|
||||
);
|
||||
|
||||
if (validData.length === 0) {
|
||||
return (
|
||||
<Card title="Pareto Front Evolution">
|
||||
<div className="h-80 flex items-center justify-center text-dark-300">
|
||||
No Pareto front data yet
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Sort data by x-coordinate for Pareto front line
|
||||
const sortedData = [...validData].sort((a, b) => a[xKey] - b[xKey]);
|
||||
|
||||
return (
|
||||
<Card title="Pareto Front Evolution">
|
||||
<div className="h-80">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ScatterChart margin={{ top: 20, right: 20, bottom: 40, left: 60 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
|
||||
<XAxis
|
||||
type="number"
|
||||
dataKey={xKey}
|
||||
name={xKey}
|
||||
stroke="#94a3b8"
|
||||
label={{ value: xKey, position: 'insideBottom', offset: -30, fill: '#94a3b8' }}
|
||||
/>
|
||||
<YAxis
|
||||
type="number"
|
||||
dataKey={yKey}
|
||||
name={yKey}
|
||||
stroke="#94a3b8"
|
||||
label={{ value: yKey, angle: -90, position: 'insideLeft', offset: -40, fill: '#94a3b8' }}
|
||||
/>
|
||||
{zKey && <ZAxis type="number" dataKey={zKey} range={[50, 400]} name={zKey} />}
|
||||
<Tooltip
|
||||
cursor={{ strokeDasharray: '3 3' }}
|
||||
contentStyle={{ backgroundColor: '#1e293b', border: 'none', borderRadius: '8px' }}
|
||||
labelStyle={{ color: '#e2e8f0' }}
|
||||
formatter={(value: any) => {
|
||||
if (typeof value === 'number') {
|
||||
return value.toFixed(2);
|
||||
}
|
||||
return value;
|
||||
}}
|
||||
/>
|
||||
{/* Pareto front line */}
|
||||
<Line
|
||||
type="monotone"
|
||||
data={sortedData}
|
||||
dataKey={yKey}
|
||||
stroke="#8b5cf6"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
connectNulls={false}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<Scatter name="Pareto Front" data={validData} fill="#8884d8">
|
||||
{validData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.isPareto !== false ? '#10b981' : '#60a5fa'} r={6} />
|
||||
))}
|
||||
</Scatter>
|
||||
</ScatterChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,120 @@
|
||||
import { useState } from 'react';
|
||||
import { Card } from '../common/Card';
|
||||
import { Button } from '../common/Button';
|
||||
import { Input } from '../common/Input';
|
||||
import { FileText, Download, Plus, Trash2, MoveUp, MoveDown } from 'lucide-react';
|
||||
|
||||
interface ReportSection {
|
||||
id: string;
|
||||
type: 'text' | 'chart' | 'table' | 'image';
|
||||
title: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export const ReportBuilder = () => {
|
||||
const [sections, setSections] = useState<ReportSection[]>([
|
||||
{ id: '1', type: 'text', title: 'Executive Summary', content: 'The optimization study successfully converged...' },
|
||||
{ id: '2', type: 'chart', title: 'Convergence Plot', content: 'convergence_plot' },
|
||||
{ id: '3', type: 'table', title: 'Top 10 Designs', content: 'top_designs_table' },
|
||||
]);
|
||||
|
||||
const addSection = (type: ReportSection['type']) => {
|
||||
setSections([
|
||||
...sections,
|
||||
{ id: Date.now().toString(), type, title: 'New Section', content: '' }
|
||||
]);
|
||||
};
|
||||
|
||||
const removeSection = (id: string) => {
|
||||
setSections(sections.filter(s => s.id !== id));
|
||||
};
|
||||
|
||||
const moveSection = (index: number, direction: 'up' | 'down') => {
|
||||
if (direction === 'up' && index === 0) return;
|
||||
if (direction === 'down' && index === sections.length - 1) return;
|
||||
|
||||
const newSections = [...sections];
|
||||
const targetIndex = direction === 'up' ? index - 1 : index + 1;
|
||||
[newSections[index], newSections[targetIndex]] = [newSections[targetIndex], newSections[index]];
|
||||
setSections(newSections);
|
||||
};
|
||||
|
||||
const updateSection = (id: string, field: keyof ReportSection, value: string) => {
|
||||
setSections(sections.map(s => s.id === id ? { ...s, [field]: value } : s));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-12 gap-6 h-full">
|
||||
{/* Editor Sidebar */}
|
||||
<div className="col-span-4 flex flex-col gap-4">
|
||||
<Card title="Report Structure" className="flex-1 flex flex-col">
|
||||
<div className="flex-1 overflow-y-auto space-y-3 pr-2">
|
||||
{sections.map((section, index) => (
|
||||
<div key={section.id} className="bg-dark-900/50 p-3 rounded-lg border border-dark-700 group">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-xs font-medium text-primary-400 uppercase">{section.type}</span>
|
||||
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button onClick={() => moveSection(index, 'up')} className="p-1 hover:bg-dark-700 rounded"><MoveUp className="w-3 h-3" /></button>
|
||||
<button onClick={() => moveSection(index, 'down')} className="p-1 hover:bg-dark-700 rounded"><MoveDown className="w-3 h-3" /></button>
|
||||
<button onClick={() => removeSection(section.id)} className="p-1 hover:bg-red-900/50 text-red-400 rounded"><Trash2 className="w-3 h-3" /></button>
|
||||
</div>
|
||||
</div>
|
||||
<Input
|
||||
value={section.title}
|
||||
onChange={(e) => updateSection(section.id, 'title', e.target.value)}
|
||||
className="mb-2 text-sm"
|
||||
/>
|
||||
{section.type === 'text' && (
|
||||
<textarea
|
||||
className="w-full bg-dark-800 border border-dark-600 rounded-md p-2 text-xs text-dark-100 focus:outline-none focus:border-primary-500 resize-none h-20"
|
||||
value={section.content}
|
||||
onChange={(e) => updateSection(section.id, 'content', e.target.value)}
|
||||
placeholder="Enter content..."
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 pt-4 border-t border-dark-600 grid grid-cols-2 gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={() => addSection('text')} icon={<Plus className="w-3 h-3" />}>Text</Button>
|
||||
<Button size="sm" variant="secondary" onClick={() => addSection('chart')} icon={<Plus className="w-3 h-3" />}>Chart</Button>
|
||||
<Button size="sm" variant="secondary" onClick={() => addSection('table')} icon={<Plus className="w-3 h-3" />}>Table</Button>
|
||||
<Button size="sm" variant="secondary" onClick={() => addSection('image')} icon={<Plus className="w-3 h-3" />}>Image</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Preview Area */}
|
||||
<div className="col-span-8 flex flex-col gap-4">
|
||||
<Card className="flex-1 flex flex-col bg-white text-black overflow-hidden">
|
||||
<div className="flex items-center justify-between border-b border-gray-200 pb-4 mb-6">
|
||||
<h2 className="text-2xl font-bold text-gray-900">Optimization Report Preview</h2>
|
||||
<Button size="sm" icon={<Download className="w-4 h-4" />}>Export PDF</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto pr-4 space-y-8">
|
||||
{sections.map(section => (
|
||||
<div key={section.id}>
|
||||
<h3 className="text-xl font-semibold text-gray-800 mb-3">{section.title}</h3>
|
||||
{section.type === 'text' && (
|
||||
<p className="text-gray-600 leading-relaxed">{section.content}</p>
|
||||
)}
|
||||
{section.type === 'chart' && (
|
||||
<div className="h-64 bg-gray-100 rounded-lg flex items-center justify-center border border-gray-200 border-dashed">
|
||||
<span className="text-gray-400 font-medium">[Chart Placeholder: {section.content}]</span>
|
||||
</div>
|
||||
)}
|
||||
{section.type === 'table' && (
|
||||
<div className="h-32 bg-gray-100 rounded-lg flex items-center justify-center border border-gray-200 border-dashed">
|
||||
<span className="text-gray-400 font-medium">[Table Placeholder: {section.content}]</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Study } from '../../types';
|
||||
import clsx from 'clsx';
|
||||
import { Play, CheckCircle, Clock } from 'lucide-react';
|
||||
|
||||
interface StudyCardProps {
|
||||
study: Study;
|
||||
isActive: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export const StudyCard = ({ study, isActive, onClick }: StudyCardProps) => {
|
||||
const getStatusIcon = () => {
|
||||
switch (study.status) {
|
||||
case 'running':
|
||||
return <Play className="w-4 h-4 text-green-400 animate-pulse" />;
|
||||
case 'completed':
|
||||
return <CheckCircle className="w-4 h-4 text-blue-400" />;
|
||||
default:
|
||||
return <Clock className="w-4 h-4 text-dark-400" />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={clsx(
|
||||
'p-4 rounded-lg border cursor-pointer transition-all duration-200',
|
||||
isActive
|
||||
? 'bg-primary-900/20 border-primary-500/50 shadow-md'
|
||||
: 'bg-dark-800 border-dark-600 hover:bg-dark-700 hover:border-dark-500'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<h4 className={clsx('font-medium truncate pr-2', isActive ? 'text-primary-100' : 'text-dark-100')}>
|
||||
{study.name}
|
||||
</h4>
|
||||
{getStatusIcon()}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between text-xs text-dark-300">
|
||||
<span>{study.status}</span>
|
||||
<span>
|
||||
{study.progress.current} / {study.progress.total} trials
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="mt-3 h-1.5 w-full bg-dark-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={clsx(
|
||||
"h-full rounded-full transition-all duration-500",
|
||||
study.status === 'completed' ? 'bg-blue-500' : 'bg-green-500'
|
||||
)}
|
||||
style={{ width: `${(study.progress.current / study.progress.total) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { Sidebar } from './Sidebar';
|
||||
|
||||
export const MainLayout = () => {
|
||||
return (
|
||||
<div className="min-h-screen bg-dark-900 text-dark-50 font-sans">
|
||||
<Sidebar />
|
||||
<main className="ml-64 min-h-screen">
|
||||
<div className="max-w-7xl mx-auto p-8">
|
||||
<Outlet />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { LayoutDashboard, Settings, FileText, Activity } from 'lucide-react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export const Sidebar = () => {
|
||||
const navItems = [
|
||||
{ to: '/dashboard', icon: Activity, label: 'Live Dashboard' },
|
||||
{ to: '/configurator', icon: Settings, label: 'Configurator' },
|
||||
{ to: '/results', icon: FileText, label: 'Results Viewer' },
|
||||
];
|
||||
|
||||
return (
|
||||
<aside className="w-64 bg-dark-800 border-r border-dark-600 flex flex-col h-screen fixed left-0 top-0">
|
||||
<div className="p-6 border-b border-dark-600">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-primary-600 rounded-lg flex items-center justify-center">
|
||||
<LayoutDashboard className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<h1 className="text-xl font-bold text-white tracking-tight">Atomizer</h1>
|
||||
</div>
|
||||
<p className="text-xs text-dark-300 mt-1 ml-11">Optimization Platform</p>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 p-4 space-y-1">
|
||||
{navItems.map((item) => (
|
||||
<NavLink
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
className={({ isActive }) =>
|
||||
clsx(
|
||||
'flex items-center gap-3 px-4 py-3 rounded-lg transition-colors duration-200',
|
||||
isActive
|
||||
? 'bg-primary-900/50 text-primary-100 border border-primary-700/50'
|
||||
: 'text-dark-300 hover:bg-dark-700 hover:text-white'
|
||||
)
|
||||
}
|
||||
>
|
||||
<item.icon className="w-5 h-5" />
|
||||
<span className="font-medium">{item.label}</span>
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="p-4 border-t border-dark-600">
|
||||
<div className="bg-dark-700 rounded-lg p-4">
|
||||
<div className="text-xs font-medium text-dark-400 uppercase mb-2">System Status</div>
|
||||
<div className="flex items-center gap-2 text-sm text-green-400">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
|
||||
Backend Online
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
66
atomizer-dashboard/frontend/src/hooks/useWebSocket.ts
Normal file
66
atomizer-dashboard/frontend/src/hooks/useWebSocket.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import useWebSocket, { ReadyState } from 'react-use-websocket';
|
||||
import { WebSocketMessage } from '../types';
|
||||
|
||||
interface UseOptimizationWebSocketProps {
|
||||
studyId: string | null;
|
||||
onMessage?: (message: WebSocketMessage) => void;
|
||||
}
|
||||
|
||||
export const useOptimizationWebSocket = ({ studyId, onMessage }: UseOptimizationWebSocketProps) => {
|
||||
const [socketUrl, setSocketUrl] = useState<string | null>(null);
|
||||
const messageQueue = useRef<WebSocketMessage[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (studyId) {
|
||||
// In development, we might need to point to localhost:8000 explicitly if not proxied
|
||||
// But assuming Vite proxy is set up correctly:
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const host = window.location.host; // This will be localhost:3000 in dev
|
||||
// If using proxy in vite.config.ts, this works.
|
||||
// If not, we might need to hardcode backend URL for dev:
|
||||
const backendHost = import.meta.env.DEV ? 'localhost:8000' : host;
|
||||
|
||||
setSocketUrl(`${protocol}//${backendHost}/api/ws/optimization/${studyId}`);
|
||||
} else {
|
||||
setSocketUrl(null);
|
||||
}
|
||||
}, [studyId]);
|
||||
|
||||
const { sendMessage, lastMessage, readyState } = useWebSocket(socketUrl, {
|
||||
shouldReconnect: (closeEvent) => true,
|
||||
reconnectAttempts: 10,
|
||||
reconnectInterval: 3000,
|
||||
onOpen: () => console.log('WebSocket Connected'),
|
||||
onClose: () => console.log('WebSocket Disconnected'),
|
||||
onError: (e) => console.error('WebSocket Error:', e),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (lastMessage !== null) {
|
||||
try {
|
||||
const data = JSON.parse(lastMessage.data) as WebSocketMessage;
|
||||
if (onMessage) {
|
||||
onMessage(data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to parse WebSocket message:', e);
|
||||
}
|
||||
}
|
||||
}, [lastMessage, onMessage]);
|
||||
|
||||
const connectionStatus = {
|
||||
[ReadyState.CONNECTING]: 'Connecting',
|
||||
[ReadyState.OPEN]: 'Open',
|
||||
[ReadyState.CLOSING]: 'Closing',
|
||||
[ReadyState.CLOSED]: 'Closed',
|
||||
[ReadyState.UNINSTANTIATED]: 'Uninstantiated',
|
||||
}[readyState];
|
||||
|
||||
return {
|
||||
sendMessage,
|
||||
lastMessage,
|
||||
readyState,
|
||||
connectionStatus,
|
||||
};
|
||||
};
|
||||
69
atomizer-dashboard/frontend/src/index.css
Normal file
69
atomizer-dashboard/frontend/src/index.css
Normal file
@@ -0,0 +1,69 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
@apply bg-dark-700 text-dark-50;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.card {
|
||||
@apply bg-dark-600 rounded-lg shadow-lg p-6;
|
||||
}
|
||||
|
||||
.btn {
|
||||
@apply px-4 py-2 rounded-md font-semibold transition-all duration-200;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
@apply btn bg-primary-500 text-white hover:bg-primary-600;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply btn bg-dark-500 text-dark-50 hover:bg-dark-400;
|
||||
}
|
||||
|
||||
.input {
|
||||
@apply bg-dark-500 border border-dark-400 rounded-md px-3 py-2 text-dark-50 focus:outline-none focus:ring-2 focus:ring-primary-500;
|
||||
}
|
||||
|
||||
.badge {
|
||||
@apply inline-block px-2 py-1 text-xs font-semibold rounded-full;
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
@apply badge bg-green-900 text-green-300;
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
@apply badge bg-yellow-900 text-yellow-300;
|
||||
}
|
||||
|
||||
.badge-error {
|
||||
@apply badge bg-red-900 text-red-300;
|
||||
}
|
||||
|
||||
.badge-info {
|
||||
@apply badge bg-blue-900 text-blue-300;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
@apply bg-dark-700;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
@apply bg-dark-500 rounded-full;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
@apply bg-dark-400;
|
||||
}
|
||||
10
atomizer-dashboard/frontend/src/main.tsx
Normal file
10
atomizer-dashboard/frontend/src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import './index.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
258
atomizer-dashboard/frontend/src/pages/Configurator.tsx
Normal file
258
atomizer-dashboard/frontend/src/pages/Configurator.tsx
Normal file
@@ -0,0 +1,258 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Card } from '../components/common/Card';
|
||||
import { Input } from '../components/common/Input';
|
||||
import { Button } from '../components/common/Button';
|
||||
import { Plus, Trash2, Upload, Save } from 'lucide-react';
|
||||
import { apiClient } from '../api/client';
|
||||
|
||||
interface DesignVariable {
|
||||
id: string;
|
||||
name: string;
|
||||
min: number;
|
||||
max: number;
|
||||
unit: string;
|
||||
}
|
||||
|
||||
interface Objective {
|
||||
id: string;
|
||||
name: string;
|
||||
goal: 'minimize' | 'maximize' | 'target';
|
||||
target?: number;
|
||||
weight: number;
|
||||
}
|
||||
|
||||
export default function Configurator() {
|
||||
const navigate = useNavigate();
|
||||
const [studyName, setStudyName] = useState('');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [variables, setVariables] = useState<DesignVariable[]>([
|
||||
{ id: '1', name: 'thickness', min: 2.0, max: 10.0, unit: 'mm' }
|
||||
]);
|
||||
const [objectives, setObjectives] = useState<Objective[]>([
|
||||
{ id: '1', name: 'mass', goal: 'minimize', weight: 1.0 }
|
||||
]);
|
||||
|
||||
const addVariable = () => {
|
||||
setVariables([
|
||||
...variables,
|
||||
{ id: Date.now().toString(), name: '', min: 0, max: 100, unit: '' }
|
||||
]);
|
||||
};
|
||||
|
||||
const removeVariable = (id: string) => {
|
||||
setVariables(variables.filter(v => v.id !== id));
|
||||
};
|
||||
|
||||
const updateVariable = (id: string, field: keyof DesignVariable, value: any) => {
|
||||
setVariables(variables.map(v =>
|
||||
v.id === id ? { ...v, [field]: value } : v
|
||||
));
|
||||
};
|
||||
|
||||
const handleCreateStudy = async () => {
|
||||
if (!studyName) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const config = {
|
||||
name: studyName,
|
||||
design_variables: variables.map(({ id, ...v }) => v),
|
||||
objectives: objectives.map(({ id, ...o }) => o),
|
||||
optimization_settings: {
|
||||
n_trials: 50,
|
||||
sampler: 'tpe'
|
||||
}
|
||||
};
|
||||
|
||||
await apiClient.createStudy(config);
|
||||
navigate('/dashboard');
|
||||
} catch (error) {
|
||||
console.error('Failed to create study:', error);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-5xl">
|
||||
<header className="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-primary-400">Study Configurator</h1>
|
||||
<p className="text-dark-300 mt-1">Create and configure new optimization studies</p>
|
||||
</div>
|
||||
<Button
|
||||
icon={<Save className="w-4 h-4" />}
|
||||
onClick={handleCreateStudy}
|
||||
isLoading={isSubmitting}
|
||||
disabled={!studyName}
|
||||
>
|
||||
Create Study
|
||||
</Button>
|
||||
</header>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Left Column: Basic Info & Files */}
|
||||
<div className="space-y-6 lg:col-span-1">
|
||||
<Card title="Study Details">
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
label="Study Name"
|
||||
placeholder="e.g., bracket_optimization_v1"
|
||||
value={studyName}
|
||||
onChange={(e) => setStudyName(e.target.value)}
|
||||
/>
|
||||
|
||||
<div className="pt-4 border-t border-dark-600">
|
||||
<label className="block text-sm font-medium text-dark-200 mb-2">
|
||||
Model Files
|
||||
</label>
|
||||
<div className="border-2 border-dashed border-dark-600 rounded-lg p-6 text-center hover:border-primary-500/50 hover:bg-dark-800/50 transition-colors cursor-pointer">
|
||||
<Upload className="w-8 h-8 text-dark-400 mx-auto mb-2" />
|
||||
<p className="text-sm text-dark-300">
|
||||
Drag & drop .prt, .sim, .fem files here
|
||||
</p>
|
||||
<p className="text-xs text-dark-500 mt-1">
|
||||
or click to browse
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card title="Optimization Settings">
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
label="Number of Trials"
|
||||
type="number"
|
||||
defaultValue={50}
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-dark-200">
|
||||
Sampler
|
||||
</label>
|
||||
<select className="w-full bg-dark-800 border border-dark-600 rounded-lg px-3 py-2 text-dark-50 focus:outline-none focus:ring-2 focus:ring-primary-500">
|
||||
<option value="tpe">TPE (Tree-structured Parzen Estimator)</option>
|
||||
<option value="cmaes">CMA-ES</option>
|
||||
<option value="random">Random Search</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Variables & Objectives */}
|
||||
<div className="space-y-6 lg:col-span-2">
|
||||
<Card title="Design Variables">
|
||||
<div className="space-y-4">
|
||||
{variables.map((variable) => (
|
||||
<div key={variable.id} className="flex gap-3 items-start bg-dark-900/50 p-3 rounded-lg border border-dark-700">
|
||||
<div className="flex-1 grid grid-cols-12 gap-3">
|
||||
<div className="col-span-4">
|
||||
<Input
|
||||
placeholder="Name"
|
||||
value={variable.name}
|
||||
onChange={(e) => updateVariable(variable.id, 'name', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Min"
|
||||
value={variable.min}
|
||||
onChange={(e) => updateVariable(variable.id, 'min', parseFloat(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Max"
|
||||
value={variable.max}
|
||||
onChange={(e) => updateVariable(variable.id, 'max', parseFloat(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Input
|
||||
placeholder="Unit"
|
||||
value={variable.unit}
|
||||
onChange={(e) => updateVariable(variable.id, 'unit', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => removeVariable(variable.id)}
|
||||
className="p-2 text-dark-400 hover:text-red-400 hover:bg-dark-800 rounded-lg transition-colors mt-0.5"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="w-full border-dashed"
|
||||
onClick={addVariable}
|
||||
icon={<Plus className="w-4 h-4" />}
|
||||
>
|
||||
Add Variable
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card title="Objectives">
|
||||
<div className="space-y-4">
|
||||
{objectives.map((objective) => (
|
||||
<div key={objective.id} className="flex gap-3 items-start bg-dark-900/50 p-3 rounded-lg border border-dark-700">
|
||||
<div className="flex-1 grid grid-cols-12 gap-3">
|
||||
<div className="col-span-5">
|
||||
<Input
|
||||
placeholder="Name (e.g., mass, stress)"
|
||||
value={objective.name}
|
||||
onChange={(e) => {
|
||||
const newObjectives = objectives.map(o =>
|
||||
o.id === objective.id ? { ...o, name: e.target.value } : o
|
||||
);
|
||||
setObjectives(newObjectives);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-4">
|
||||
<select
|
||||
className="w-full bg-dark-800 border border-dark-600 rounded-lg px-3 py-2 text-dark-50 focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
value={objective.goal}
|
||||
onChange={(e) => {
|
||||
const newObjectives = objectives.map(o =>
|
||||
o.id === objective.id ? { ...o, goal: e.target.value as any } : o
|
||||
);
|
||||
setObjectives(newObjectives);
|
||||
}}
|
||||
>
|
||||
<option value="minimize">Minimize</option>
|
||||
<option value="maximize">Maximize</option>
|
||||
<option value="target">Target Value</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Weight"
|
||||
value={objective.weight}
|
||||
onChange={(e) => {
|
||||
const newObjectives = objectives.map(o =>
|
||||
o.id === objective.id ? { ...o, weight: parseFloat(e.target.value) } : o
|
||||
);
|
||||
setObjectives(newObjectives);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -28,6 +28,7 @@ export default function Dashboard() {
|
||||
// Protocol 13: New state for metadata and Pareto front
|
||||
const [studyMetadata, setStudyMetadata] = useState<any>(null);
|
||||
const [paretoFront, setParetoFront] = useState<any[]>([]);
|
||||
const [allTrialsRaw, setAllTrialsRaw] = useState<any[]>([]); // All trials for parallel coordinates
|
||||
|
||||
// Load studies on mount
|
||||
useEffect(() => {
|
||||
@@ -117,13 +118,32 @@ export default function Dashboard() {
|
||||
fetch(`/api/optimization/studies/${selectedStudyId}/pareto-front`)
|
||||
.then(res => res.json())
|
||||
.then(paretoData => {
|
||||
console.log('[Dashboard] Pareto front data:', paretoData);
|
||||
if (paretoData.is_multi_objective && paretoData.pareto_front) {
|
||||
console.log('[Dashboard] Setting Pareto front with', paretoData.pareto_front.length, 'trials');
|
||||
setParetoFront(paretoData.pareto_front);
|
||||
} else {
|
||||
console.log('[Dashboard] No Pareto front or not multi-objective');
|
||||
setParetoFront([]);
|
||||
}
|
||||
})
|
||||
.catch(err => console.error('Failed to load Pareto front:', err));
|
||||
|
||||
// Fetch ALL trials (not just Pareto) for parallel coordinates
|
||||
fetch(`/api/optimization/studies/${selectedStudyId}/history`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
// Transform to match the format expected by ParallelCoordinatesPlot
|
||||
const trialsData = data.trials.map((t: any) => ({
|
||||
trial_number: t.trial_number,
|
||||
values: t.values || [],
|
||||
params: t.design_variables || {},
|
||||
user_attrs: t.user_attrs || {},
|
||||
constraint_satisfied: t.constraint_satisfied !== false
|
||||
}));
|
||||
setAllTrialsRaw(trialsData);
|
||||
})
|
||||
.catch(err => console.error('Failed to load all trials:', err));
|
||||
}
|
||||
}, [selectedStudyId]);
|
||||
|
||||
@@ -275,13 +295,12 @@ export default function Dashboard() {
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
if (selectedStudyId) {
|
||||
window.open(`http://localhost:8080?study=${selectedStudyId}`, '_blank');
|
||||
}
|
||||
// Open Optuna dashboard on port 8081
|
||||
// Note: The dashboard needs to be started separately with the correct study database
|
||||
window.open('http://localhost:8081', '_blank');
|
||||
}}
|
||||
className="btn-secondary"
|
||||
disabled={!selectedStudyId}
|
||||
title="Open Optuna Dashboard (make sure it's running on port 8080)"
|
||||
title="Open Optuna Dashboard (runs on port 8081)"
|
||||
>
|
||||
Optuna Dashboard
|
||||
</button>
|
||||
@@ -355,17 +374,19 @@ export default function Dashboard() {
|
||||
<ParetoPlot
|
||||
paretoData={paretoFront}
|
||||
objectives={studyMetadata.objectives}
|
||||
allTrials={allTrialsRaw}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Parallel Coordinates (full width for multi-objective) */}
|
||||
{paretoFront.length > 0 && studyMetadata && studyMetadata.objectives && studyMetadata.design_variables && (
|
||||
{allTrialsRaw.length > 0 && studyMetadata && studyMetadata.objectives && studyMetadata.design_variables && (
|
||||
<div className="mb-6">
|
||||
<ParallelCoordinatesPlot
|
||||
paretoData={paretoFront}
|
||||
paretoData={allTrialsRaw}
|
||||
objectives={studyMetadata.objectives}
|
||||
designVariables={studyMetadata.design_variables}
|
||||
paretoFront={paretoFront}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
151
atomizer-dashboard/frontend/src/pages/Results.tsx
Normal file
151
atomizer-dashboard/frontend/src/pages/Results.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Card } from '../components/common/Card';
|
||||
import { Button } from '../components/common/Button';
|
||||
import { Download, FileText, Image, RefreshCw } from 'lucide-react';
|
||||
import { apiClient } from '../api/client';
|
||||
import { Study } from '../types';
|
||||
|
||||
export default function Results() {
|
||||
const [studies, setStudies] = useState<Study[]>([]);
|
||||
const [selectedStudyId, setSelectedStudyId] = useState<string | null>(null);
|
||||
const [reportContent, setReportContent] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
apiClient.getStudies()
|
||||
.then(data => {
|
||||
setStudies(data.studies);
|
||||
if (data.studies.length > 0) {
|
||||
const completed = data.studies.find(s => s.status === 'completed');
|
||||
setSelectedStudyId(completed?.id || data.studies[0].id);
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedStudyId) {
|
||||
setLoading(true);
|
||||
apiClient.getStudyReport(selectedStudyId)
|
||||
.then(data => {
|
||||
setReportContent(data.content);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to fetch report:', err);
|
||||
// Fallback for demo if report doesn't exist
|
||||
setReportContent(`# Optimization Report: ${selectedStudyId}
|
||||
|
||||
## Executive Summary
|
||||
The optimization study successfully converged after 45 trials. The best design achieved a mass reduction of 15% while maintaining all constraints.
|
||||
|
||||
## Key Findings
|
||||
- **Best Objective Value**: 115.185 Hz
|
||||
- **Critical Parameter**: Plate Thickness (sensitivity: 0.85)
|
||||
- **Constraint Margins**: All safety factors > 1.2
|
||||
|
||||
## Recommendations
|
||||
Based on the results, we recommend proceeding with the design from Trial #45. Further refinement could be achieved by narrowing the bounds for 'thickness'.
|
||||
`);
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
}, [selectedStudyId]);
|
||||
|
||||
const handleRegenerate = () => {
|
||||
if (!selectedStudyId) return;
|
||||
setLoading(true);
|
||||
// In a real app, this would call an endpoint to trigger report generation
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto h-[calc(100vh-100px)] flex flex-col">
|
||||
<header className="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-primary-400">Results Viewer</h1>
|
||||
<p className="text-dark-300 mt-1">Analyze completed optimization studies</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon={<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />}
|
||||
onClick={handleRegenerate}
|
||||
disabled={loading || !selectedStudyId}
|
||||
>
|
||||
Regenerate
|
||||
</Button>
|
||||
<Button variant="secondary" icon={<Download className="w-4 h-4" />}>
|
||||
Export Data
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="grid grid-cols-12 gap-6 flex-1 min-h-0">
|
||||
{/* Sidebar - Study Selection */}
|
||||
<aside className="col-span-3 flex flex-col gap-4">
|
||||
<Card title="Select Study" className="flex-1 overflow-hidden flex flex-col">
|
||||
<div className="space-y-2 overflow-y-auto flex-1 pr-2">
|
||||
{studies.map(study => (
|
||||
<button
|
||||
key={study.id}
|
||||
onClick={() => setSelectedStudyId(study.id)}
|
||||
className={`w-full text-left p-3 rounded-lg transition-colors ${
|
||||
selectedStudyId === study.id
|
||||
? 'bg-primary-900/30 text-primary-100 border border-primary-700/50'
|
||||
: 'text-dark-300 hover:bg-dark-700'
|
||||
}`}
|
||||
>
|
||||
<div className="font-medium truncate">{study.name}</div>
|
||||
<div className="text-xs text-dark-400 mt-1 capitalize">{study.status}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</aside>
|
||||
|
||||
{/* Main Content - Report Viewer */}
|
||||
<main className="col-span-9 flex flex-col gap-6 overflow-hidden">
|
||||
<Card className="flex-1 overflow-hidden flex flex-col">
|
||||
<div className="flex items-center justify-between border-b border-dark-600 pb-4 mb-4">
|
||||
<h2 className="text-xl font-semibold text-white flex items-center gap-2">
|
||||
<FileText className="w-5 h-5 text-primary-400" />
|
||||
Optimization Report
|
||||
</h2>
|
||||
<div className="flex gap-2">
|
||||
<button className="p-2 text-dark-300 hover:text-white hover:bg-dark-700 rounded-lg" title="View Charts">
|
||||
<Image className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto pr-4 custom-scrollbar">
|
||||
{loading ? (
|
||||
<div className="h-full flex items-center justify-center text-dark-300">
|
||||
<RefreshCw className="w-8 h-8 animate-spin mb-2" />
|
||||
<span className="ml-2">Loading report...</span>
|
||||
</div>
|
||||
) : reportContent ? (
|
||||
<div className="prose prose-invert max-w-none">
|
||||
{/* Simple markdown rendering for now */}
|
||||
{reportContent.split('\n').map((line, i) => {
|
||||
if (line.startsWith('# ')) return <h1 key={i} className="text-2xl font-bold text-white mt-6 mb-4">{line.substring(2)}</h1>;
|
||||
if (line.startsWith('## ')) return <h2 key={i} className="text-xl font-bold text-primary-200 mt-6 mb-3">{line.substring(3)}</h2>;
|
||||
if (line.startsWith('- ')) return <li key={i} className="ml-4 text-dark-100">{line.substring(2)}</li>;
|
||||
return <p key={i} className="text-dark-200 mb-2">{line}</p>;
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full flex items-center justify-center text-dark-300">
|
||||
Select a study to view results
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
123
atomizer-dashboard/frontend/src/types/index.ts
Normal file
123
atomizer-dashboard/frontend/src/types/index.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
// Study types
|
||||
export interface Study {
|
||||
id: string;
|
||||
name: string;
|
||||
status: 'not_started' | 'running' | 'completed';
|
||||
progress: {
|
||||
current: number;
|
||||
total: number;
|
||||
};
|
||||
best_value: number | null;
|
||||
target: number | null;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface StudyListResponse {
|
||||
studies: Study[];
|
||||
}
|
||||
|
||||
// Trial types
|
||||
export interface DesignVariables {
|
||||
[key: string]: number;
|
||||
}
|
||||
|
||||
export interface Results {
|
||||
first_frequency?: number;
|
||||
[key: string]: number | undefined;
|
||||
}
|
||||
|
||||
export interface Trial {
|
||||
trial_number: number;
|
||||
objective: number;
|
||||
design_variables: DesignVariables;
|
||||
results: Results;
|
||||
timestamp?: string;
|
||||
user_attrs?: {
|
||||
[key: string]: any;
|
||||
};
|
||||
start_time?: string;
|
||||
end_time?: string;
|
||||
objectives?: number[];
|
||||
}
|
||||
|
||||
export interface HistoryResponse {
|
||||
trials: Trial[];
|
||||
study_id: string;
|
||||
}
|
||||
|
||||
// Pruning types
|
||||
export interface PrunedTrial {
|
||||
trial_number: number;
|
||||
pruning_cause: string;
|
||||
design_variables: DesignVariables;
|
||||
timestamp?: string;
|
||||
diagnostics?: {
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
export interface PruningResponse {
|
||||
pruned_trials: PrunedTrial[];
|
||||
study_id: string;
|
||||
}
|
||||
|
||||
// WebSocket message types
|
||||
export type WebSocketMessageType =
|
||||
| 'connected'
|
||||
| 'trial_completed'
|
||||
| 'new_best'
|
||||
| 'progress'
|
||||
| 'trial_pruned';
|
||||
|
||||
export interface WebSocketMessage {
|
||||
type: WebSocketMessageType;
|
||||
data: any;
|
||||
}
|
||||
|
||||
export interface ConnectedMessage {
|
||||
study_id: string;
|
||||
current_trials: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface TrialCompletedMessage extends Trial {}
|
||||
|
||||
export interface NewBestMessage extends Trial {}
|
||||
|
||||
export interface ProgressMessage {
|
||||
current: number;
|
||||
total: number;
|
||||
percentage: number;
|
||||
}
|
||||
|
||||
export interface TrialPrunedMessage extends PrunedTrial {}
|
||||
|
||||
// Chart data types
|
||||
export interface ConvergenceDataPoint {
|
||||
trial_number: number;
|
||||
objective: number;
|
||||
best_so_far: number;
|
||||
}
|
||||
|
||||
export interface ParameterSpaceDataPoint {
|
||||
trial_number: number;
|
||||
x: number;
|
||||
y: number;
|
||||
objective: number;
|
||||
isBest: boolean;
|
||||
}
|
||||
|
||||
// Study status types
|
||||
export interface StudyStatus {
|
||||
study_id: string;
|
||||
status: 'not_started' | 'running' | 'completed';
|
||||
progress: {
|
||||
current: number;
|
||||
total: number;
|
||||
};
|
||||
best_trial: Trial | null;
|
||||
pruned_count: number;
|
||||
config: {
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
1
atomizer-dashboard/frontend/src/vite-env.d.ts
vendored
Normal file
1
atomizer-dashboard/frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
Reference in New Issue
Block a user