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:
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user