Files
Atomizer/atomizer-dashboard/frontend/src/pages/Setup.tsx
Anto01 ba0b9a1fae feat(dashboard): Enhanced chat, spec management, and Claude integration
Backend:
- spec.py: New AtomizerSpec REST API endpoints
- spec_manager.py: SpecManager service for unified config
- interview_engine.py: Study creation interview logic
- claude.py: Enhanced Claude API with context
- optimization.py: Extended optimization endpoints
- context_builder.py, session_manager.py: Improved services

Frontend:
- Chat components: Enhanced message rendering, tool call cards
- Hooks: useClaudeCode, useSpecWebSocket, improved useChat
- Pages: Updated Dashboard, Analysis, Insights, Setup, Home
- Components: ParallelCoordinatesPlot, ParetoPlot improvements
- App.tsx: Route updates for canvas/studio

Infrastructure:
- vite.config.ts: Build configuration updates
- start/stop-dashboard.bat: Script improvements
2026-01-20 13:10:47 -05:00

872 lines
36 KiB
TypeScript

import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Settings,
Target,
Sliders,
AlertTriangle,
Cpu,
Box,
Layers,
Play,
Download,
RefreshCw,
ChevronDown,
ChevronUp,
ArrowUp,
ArrowDown,
CheckCircle,
Info,
FileBox,
FolderOpen,
File,
Layout,
Grid3X3
} from 'lucide-react';
import { useStudy } from '../context/StudyContext';
import { Card } from '../components/common/Card';
import { Button } from '../components/common/Button';
import { apiClient, ModelFile } from '../api/client';
import { AtomizerCanvas } from '../components/canvas/AtomizerCanvas';
import { ConfigImporter } from '../components/canvas/panels/ConfigImporter';
import { useCanvasStore } from '../hooks/useCanvasStore';
interface StudyConfig {
study_name: string;
description?: string;
objectives: {
name: string;
direction: 'minimize' | 'maximize';
unit?: string;
target?: number;
weight?: number;
}[];
design_variables: {
name: string;
type: 'float' | 'int' | 'categorical';
low?: number;
high?: number;
step?: number;
choices?: string[];
unit?: string;
}[];
constraints: {
name: string;
type: 'le' | 'ge' | 'eq';
bound: number;
unit?: string;
}[];
algorithm: {
name: string;
sampler: string;
pruner?: string;
n_trials: number;
timeout?: number;
};
fea_model?: {
software: string;
solver: string;
sim_file?: string;
mesh_elements?: number;
};
extractors?: {
name: string;
type: string;
source?: string;
}[];
}
type TabType = 'config' | 'canvas';
export default function Setup() {
const navigate = useNavigate();
const { selectedStudy, isInitialized } = useStudy();
const [activeTab, setActiveTab] = useState<TabType>('config');
const [config, setConfig] = useState<StudyConfig | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [expandedSections, setExpandedSections] = useState<Set<string>>(
new Set(['objectives', 'variables', 'constraints', 'algorithm', 'modelFiles'])
);
const [modelFiles, setModelFiles] = useState<ModelFile[]>([]);
const [modelDir, setModelDir] = useState<string>('');
const [showImporter, setShowImporter] = useState(false);
const [canvasNotification, setCanvasNotification] = useState<string | null>(null);
const { nodes } = useCanvasStore();
// Redirect if no study selected
useEffect(() => {
if (isInitialized && !selectedStudy) {
navigate('/');
}
}, [selectedStudy, navigate, isInitialized]);
// Load study configuration
useEffect(() => {
if (selectedStudy) {
loadConfig();
loadModelFiles();
}
}, [selectedStudy]);
const loadModelFiles = async () => {
if (!selectedStudy) return;
try {
const data = await apiClient.getModelFiles(selectedStudy.id);
setModelFiles(data.files);
setModelDir(data.model_dir);
} catch (err) {
console.error('Failed to load model files:', err);
}
};
const handleOpenFolder = async () => {
if (!selectedStudy) return;
try {
await apiClient.openFolder(selectedStudy.id, 'model');
} catch (err: any) {
setError(err.message || 'Failed to open folder');
}
};
const loadConfig = async () => {
if (!selectedStudy) return;
setLoading(true);
setError(null);
try {
const response = await apiClient.getStudyConfig(selectedStudy.id);
const rawConfig = response.config;
// Transform backend config format to our StudyConfig format
const transformedConfig: StudyConfig = {
study_name: rawConfig.study_name || selectedStudy.name || selectedStudy.id,
description: rawConfig.description,
objectives: (rawConfig.objectives || []).map((obj: any) => ({
name: obj.name,
direction: obj.direction || 'minimize',
unit: obj.unit || obj.units,
target: obj.target,
weight: obj.weight
})),
design_variables: (rawConfig.design_variables || []).map((dv: any) => ({
name: dv.name,
type: dv.type || 'float',
low: dv.min ?? dv.low,
high: dv.max ?? dv.high,
step: dv.step,
choices: dv.choices,
unit: dv.unit || dv.units
})),
constraints: (rawConfig.constraints || []).map((c: any) => ({
name: c.name,
type: c.type || 'le',
bound: c.max_value ?? c.min_value ?? c.bound ?? 0,
unit: c.unit || c.units
})),
algorithm: {
name: rawConfig.optimizer?.name || rawConfig.algorithm?.name || 'Optuna',
sampler: rawConfig.optimization?.algorithm || rawConfig.optimization_settings?.sampler || rawConfig.algorithm?.sampler || 'TPESampler',
pruner: rawConfig.optimization_settings?.pruner || rawConfig.algorithm?.pruner,
n_trials: rawConfig.optimization?.n_trials || rawConfig.optimization_settings?.n_trials || rawConfig.trials?.n_trials || selectedStudy.progress.total,
timeout: rawConfig.optimization_settings?.timeout
},
fea_model: rawConfig.fea_model || rawConfig.solver ? {
software: rawConfig.fea_model?.software || rawConfig.solver?.type || 'NX Nastran',
solver: rawConfig.fea_model?.solver || rawConfig.solver?.name || 'SOL 103',
sim_file: rawConfig.sim_file || rawConfig.fea_model?.sim_file,
mesh_elements: rawConfig.fea_model?.mesh_elements
} : undefined,
extractors: rawConfig.extractors
};
setConfig(transformedConfig);
} catch (err: any) {
// If no config endpoint, create mock from available data
setConfig({
study_name: selectedStudy.name || selectedStudy.id,
objectives: [{ name: 'objective', direction: 'minimize' }],
design_variables: [],
constraints: [],
algorithm: {
name: 'Optuna',
sampler: 'TPESampler',
n_trials: selectedStudy.progress.total
}
});
setError('Configuration loaded with limited data');
} finally {
setLoading(false);
}
};
const toggleSection = (section: string) => {
setExpandedSections(prev => {
const next = new Set(prev);
if (next.has(section)) {
next.delete(section);
} else {
next.add(section);
}
return next;
});
};
const handleStartOptimization = async () => {
if (!selectedStudy) return;
try {
await apiClient.startOptimization(selectedStudy.id);
navigate('/dashboard');
} catch (err: any) {
setError(err.message || 'Failed to start optimization');
}
};
const handleExportConfig = () => {
if (!config) return;
const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${selectedStudy?.id || 'study'}_config.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
// Loading state
if (!isInitialized) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<div className="animate-spin w-8 h-8 border-2 border-primary-500 border-t-transparent rounded-full mx-auto mb-4"></div>
<p className="text-dark-400">Loading study...</p>
</div>
</div>
);
}
if (!selectedStudy) return null;
// Calculate design space size
const designSpaceSize = config?.design_variables.reduce((acc, v) => {
if (v.type === 'categorical' && v.choices) {
return acc * v.choices.length;
} else if (v.type === 'int' && v.low !== undefined && v.high !== undefined) {
return acc * (v.high - v.low + 1);
}
return acc * 1000; // Approximate for continuous
}, 1) || 0;
// Canvas tab - full height and full width
if (activeTab === 'canvas') {
const handleImport = (source: string) => {
setCanvasNotification(`Loaded: ${source}`);
setTimeout(() => setCanvasNotification(null), 3000);
};
return (
<div className="w-full h-[calc(100vh-6rem)] flex flex-col -mx-6 -my-6 px-4 pt-4">
{/* Tab Bar */}
<div className="flex items-center gap-2 mb-3 border-b border-dark-700 pb-3">
<button
onClick={() => setActiveTab('config')}
className="flex items-center gap-2 px-4 py-2 rounded-lg transition-colors bg-dark-800 text-dark-300 hover:text-white hover:bg-dark-700"
>
<Layout className="w-4 h-4" />
Configuration
</button>
<button
onClick={() => navigate(`/canvas/${selectedStudy?.id || ''}`)}
className="flex items-center gap-2 px-4 py-2 rounded-lg transition-colors bg-primary-600 text-white"
>
<Grid3X3 className="w-4 h-4" />
Canvas Builder
</button>
<div className="flex-1" />
{nodes.length === 0 && (
<button
onClick={() => setShowImporter(true)}
className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-emerald-600 hover:bg-emerald-500 text-white text-sm transition-colors"
>
<Download className="w-4 h-4" />
Load Study Config
</button>
)}
<span className="text-dark-400 text-sm">
{selectedStudy?.name || 'Study'}
</span>
</div>
{/* Canvas - takes remaining height */}
<div className="flex-1 min-h-0 rounded-lg overflow-hidden border border-dark-700">
<AtomizerCanvas />
</div>
{/* Config Importer Modal */}
<ConfigImporter
isOpen={showImporter}
onClose={() => setShowImporter(false)}
onImport={handleImport}
currentStudyId={selectedStudy?.id}
/>
{/* Notification Toast */}
{canvasNotification && (
<div className="fixed bottom-4 left-1/2 transform -translate-x-1/2 px-4 py-2 bg-dark-800 text-white rounded-lg shadow-lg z-50 border border-dark-600">
{canvasNotification}
</div>
)}
</div>
);
}
return (
<div className="w-full">
{/* Tab Bar */}
<div className="flex items-center gap-2 mb-4">
<button
onClick={() => setActiveTab('config')}
className="flex items-center gap-2 px-4 py-2 rounded-lg transition-colors bg-primary-600 text-white"
>
<Layout className="w-4 h-4" />
Configuration
</button>
<button
onClick={() => navigate(`/canvas/${selectedStudy?.id || ''}`)}
className="flex items-center gap-2 px-4 py-2 rounded-lg transition-colors bg-dark-800 text-dark-300 hover:text-white hover:bg-dark-700"
>
<Grid3X3 className="w-4 h-4" />
Canvas Builder
</button>
</div>
{/* Header */}
<header className="mb-6 flex items-center justify-between border-b border-dark-600 pb-4">
<div>
<div className="flex items-center gap-3">
<Settings className="w-8 h-8 text-primary-400" />
<div>
<h1 className="text-2xl font-bold text-white">{config?.study_name || selectedStudy.name}</h1>
<p className="text-dark-400 text-sm">Study Configuration</p>
</div>
</div>
{config?.description && (
<p className="text-dark-300 mt-2 max-w-2xl">{config.description}</p>
)}
</div>
<div className="flex items-center gap-3">
<Button
variant="secondary"
icon={<RefreshCw className="w-4 h-4" />}
onClick={loadConfig}
disabled={loading}
>
Refresh
</Button>
<Button
variant="secondary"
icon={<Download className="w-4 h-4" />}
onClick={handleExportConfig}
disabled={!config}
>
Export
</Button>
{selectedStudy.status === 'not_started' && (
<Button
variant="primary"
icon={<Play className="w-4 h-4" />}
onClick={handleStartOptimization}
>
Start Optimization
</Button>
)}
</div>
</header>
{/* Error Message */}
{error && (
<div className="mb-4 p-3 bg-yellow-900/20 border border-yellow-800/30 rounded-lg">
<div className="flex items-center gap-2 text-yellow-400 text-sm">
<Info className="w-4 h-4" />
<span>{error}</span>
</div>
</div>
)}
{loading ? (
<div className="flex items-center justify-center py-16">
<RefreshCw className="w-8 h-8 animate-spin text-dark-400" />
</div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Left Column */}
<div className="space-y-6">
{/* Objectives Panel */}
<Card className="overflow-hidden">
<button
onClick={() => toggleSection('objectives')}
className="w-full flex items-center justify-between p-4 hover:bg-dark-750 transition-colors"
>
<div className="flex items-center gap-3">
<Target className="w-5 h-5 text-primary-400" />
<h2 className="text-lg font-semibold text-white">Objectives</h2>
<span className="text-xs bg-dark-600 text-dark-300 px-2 py-0.5 rounded-full">
{config?.objectives.length || 0}
</span>
</div>
{expandedSections.has('objectives') ? (
<ChevronUp className="w-5 h-5 text-dark-400" />
) : (
<ChevronDown className="w-5 h-5 text-dark-400" />
)}
</button>
{expandedSections.has('objectives') && (
<div className="px-4 pb-4 space-y-3">
{config?.objectives.map((obj, idx) => (
<div
key={idx}
className="bg-dark-750 rounded-lg p-4 border border-dark-600"
>
<div className="flex items-center justify-between mb-2">
<span className="font-medium text-white">{obj.name}</span>
<span className={`flex items-center gap-1 text-sm px-2 py-1 rounded ${
obj.direction === 'minimize'
? 'bg-green-500/10 text-green-400'
: 'bg-blue-500/10 text-blue-400'
}`}>
{obj.direction === 'minimize' ? (
<ArrowDown className="w-3 h-3" />
) : (
<ArrowUp className="w-3 h-3" />
)}
{obj.direction}
</span>
</div>
<div className="flex items-center gap-4 text-sm text-dark-400">
{obj.unit && <span>Unit: {obj.unit}</span>}
{obj.target !== undefined && (
<span>Target: {obj.target}</span>
)}
{obj.weight !== undefined && (
<span>Weight: {obj.weight}</span>
)}
</div>
</div>
))}
{config?.objectives.length === 0 && (
<p className="text-dark-500 text-sm italic">No objectives configured</p>
)}
<div className="text-xs text-dark-500 pt-2">
Type: {(config?.objectives.length || 0) > 1 ? 'Multi-Objective' : 'Single-Objective'}
</div>
</div>
)}
</Card>
{/* Design Variables Panel */}
<Card className="overflow-hidden">
<button
onClick={() => toggleSection('variables')}
className="w-full flex items-center justify-between p-4 hover:bg-dark-750 transition-colors"
>
<div className="flex items-center gap-3">
<Sliders className="w-5 h-5 text-primary-400" />
<h2 className="text-lg font-semibold text-white">Design Variables</h2>
<span className="text-xs bg-dark-600 text-dark-300 px-2 py-0.5 rounded-full">
{config?.design_variables.length || 0}
</span>
</div>
{expandedSections.has('variables') ? (
<ChevronUp className="w-5 h-5 text-dark-400" />
) : (
<ChevronDown className="w-5 h-5 text-dark-400" />
)}
</button>
{expandedSections.has('variables') && (
<div className="px-4 pb-4 space-y-2">
{config?.design_variables.map((v, idx) => (
<div
key={idx}
className="bg-dark-750 rounded-lg p-3 border border-dark-600"
>
<div className="flex items-center justify-between">
<span className="font-medium text-white font-mono text-sm">{v.name}</span>
<span className="text-xs bg-dark-600 text-dark-400 px-2 py-0.5 rounded">
{v.type}
</span>
</div>
<div className="flex items-center gap-4 text-sm text-dark-400 mt-1">
{v.low !== undefined && v.high !== undefined && (
<span>Range: [{v.low}, {v.high}]</span>
)}
{v.step && <span>Step: {v.step}</span>}
{v.unit && <span>{v.unit}</span>}
{v.choices && (
<span>Choices: {v.choices.join(', ')}</span>
)}
</div>
</div>
))}
{config?.design_variables.length === 0 && (
<p className="text-dark-500 text-sm italic">No design variables configured</p>
)}
{designSpaceSize > 0 && (
<div className="text-xs text-dark-500 pt-2">
Design Space: ~{designSpaceSize.toExponential(2)} combinations
</div>
)}
</div>
)}
</Card>
{/* Constraints Panel */}
<Card className="overflow-hidden">
<button
onClick={() => toggleSection('constraints')}
className="w-full flex items-center justify-between p-4 hover:bg-dark-750 transition-colors"
>
<div className="flex items-center gap-3">
<AlertTriangle className="w-5 h-5 text-yellow-400" />
<h2 className="text-lg font-semibold text-white">Constraints</h2>
<span className="text-xs bg-dark-600 text-dark-300 px-2 py-0.5 rounded-full">
{config?.constraints.length || 0}
</span>
</div>
{expandedSections.has('constraints') ? (
<ChevronUp className="w-5 h-5 text-dark-400" />
) : (
<ChevronDown className="w-5 h-5 text-dark-400" />
)}
</button>
{expandedSections.has('constraints') && (
<div className="px-4 pb-4">
{(config?.constraints.length || 0) > 0 ? (
<table className="w-full text-sm">
<thead>
<tr className="text-dark-400 text-left">
<th className="pb-2">Name</th>
<th className="pb-2">Type</th>
<th className="pb-2">Bound</th>
<th className="pb-2">Unit</th>
</tr>
</thead>
<tbody className="text-dark-300">
{config?.constraints.map((c, idx) => (
<tr key={idx} className="border-t border-dark-700">
<td className="py-2 font-mono">{c.name}</td>
<td className="py-2">
{c.type === 'le' ? '≤' : c.type === 'ge' ? '≥' : '='}
</td>
<td className="py-2">{c.bound}</td>
<td className="py-2 text-dark-500">{c.unit || '-'}</td>
</tr>
))}
</tbody>
</table>
) : (
<p className="text-dark-500 text-sm italic">No constraints configured</p>
)}
</div>
)}
</Card>
</div>
{/* Right Column */}
<div className="space-y-6">
{/* Algorithm Configuration */}
<Card className="overflow-hidden">
<button
onClick={() => toggleSection('algorithm')}
className="w-full flex items-center justify-between p-4 hover:bg-dark-750 transition-colors"
>
<div className="flex items-center gap-3">
<Cpu className="w-5 h-5 text-primary-400" />
<h2 className="text-lg font-semibold text-white">Algorithm Configuration</h2>
</div>
{expandedSections.has('algorithm') ? (
<ChevronUp className="w-5 h-5 text-dark-400" />
) : (
<ChevronDown className="w-5 h-5 text-dark-400" />
)}
</button>
{expandedSections.has('algorithm') && (
<div className="px-4 pb-4 space-y-3">
<div className="grid grid-cols-2 gap-4">
<div className="bg-dark-750 rounded-lg p-3 border border-dark-600">
<div className="text-xs text-dark-400 uppercase mb-1">Optimizer</div>
<div className="text-white font-medium">{config?.algorithm.name || 'Optuna'}</div>
</div>
<div className="bg-dark-750 rounded-lg p-3 border border-dark-600">
<div className="text-xs text-dark-400 uppercase mb-1">Sampler</div>
<div className="text-white font-medium">{config?.algorithm.sampler || 'TPE'}</div>
</div>
<div className="bg-dark-750 rounded-lg p-3 border border-dark-600">
<div className="text-xs text-dark-400 uppercase mb-1">Total Trials</div>
<div className="text-white font-medium">{config?.algorithm.n_trials || selectedStudy.progress.total}</div>
</div>
{config?.algorithm.pruner && (
<div className="bg-dark-750 rounded-lg p-3 border border-dark-600">
<div className="text-xs text-dark-400 uppercase mb-1">Pruner</div>
<div className="text-white font-medium">{config.algorithm.pruner}</div>
</div>
)}
{config?.algorithm.timeout && (
<div className="bg-dark-750 rounded-lg p-3 border border-dark-600">
<div className="text-xs text-dark-400 uppercase mb-1">Timeout</div>
<div className="text-white font-medium">{config.algorithm.timeout}s</div>
</div>
)}
</div>
</div>
)}
</Card>
{/* FEA Model Info */}
{config?.fea_model && (
<Card className="overflow-hidden">
<button
onClick={() => toggleSection('model')}
className="w-full flex items-center justify-between p-4 hover:bg-dark-750 transition-colors"
>
<div className="flex items-center gap-3">
<Box className="w-5 h-5 text-primary-400" />
<h2 className="text-lg font-semibold text-white">FEA Model</h2>
</div>
{expandedSections.has('model') ? (
<ChevronUp className="w-5 h-5 text-dark-400" />
) : (
<ChevronDown className="w-5 h-5 text-dark-400" />
)}
</button>
{expandedSections.has('model') && (
<div className="px-4 pb-4 space-y-3">
<div className="grid grid-cols-2 gap-4">
<div className="bg-dark-750 rounded-lg p-3 border border-dark-600">
<div className="text-xs text-dark-400 uppercase mb-1">Software</div>
<div className="text-white font-medium">{config.fea_model.software}</div>
</div>
<div className="bg-dark-750 rounded-lg p-3 border border-dark-600">
<div className="text-xs text-dark-400 uppercase mb-1">Solver</div>
<div className="text-white font-medium">{config.fea_model.solver}</div>
</div>
{config.fea_model.mesh_elements && (
<div className="bg-dark-750 rounded-lg p-3 border border-dark-600">
<div className="text-xs text-dark-400 uppercase mb-1">Mesh Elements</div>
<div className="text-white font-medium">
{config.fea_model.mesh_elements.toLocaleString()}
</div>
</div>
)}
{config.fea_model.sim_file && (
<div className="bg-dark-750 rounded-lg p-3 border border-dark-600 col-span-2">
<div className="text-xs text-dark-400 uppercase mb-1">Simulation File</div>
<div className="text-white font-mono text-sm truncate">
{config.fea_model.sim_file}
</div>
</div>
)}
</div>
</div>
)}
</Card>
)}
{/* NX Model Files */}
<Card className="overflow-hidden">
<button
onClick={() => toggleSection('modelFiles')}
className="w-full flex items-center justify-between p-4 hover:bg-dark-750 transition-colors"
>
<div className="flex items-center gap-3">
<FileBox className="w-5 h-5 text-primary-400" />
<h2 className="text-lg font-semibold text-white">NX Model Files</h2>
<span className="text-xs bg-dark-600 text-dark-300 px-2 py-0.5 rounded-full">
{modelFiles.length}
</span>
</div>
{expandedSections.has('modelFiles') ? (
<ChevronUp className="w-5 h-5 text-dark-400" />
) : (
<ChevronDown className="w-5 h-5 text-dark-400" />
)}
</button>
{expandedSections.has('modelFiles') && (
<div className="px-4 pb-4 space-y-3">
{/* Open Folder Button */}
<button
onClick={handleOpenFolder}
className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-dark-700 hover:bg-dark-600 text-dark-200 hover:text-white rounded-lg border border-dark-600 transition-colors"
>
<FolderOpen className="w-4 h-4" />
<span>Open Model Folder</span>
</button>
{/* Model Directory Path */}
{modelDir && (
<div className="text-xs text-dark-500 font-mono truncate" title={modelDir}>
{modelDir}
</div>
)}
{/* File List */}
{modelFiles.length > 0 ? (
<div className="space-y-2">
{modelFiles.map((file, idx) => (
<div
key={idx}
className="bg-dark-750 rounded-lg p-3 border border-dark-600"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<File className={`w-4 h-4 ${
file.extension === '.prt' ? 'text-blue-400' :
file.extension === '.sim' ? 'text-green-400' :
file.extension === '.fem' ? 'text-yellow-400' :
file.extension === '.bdf' || file.extension === '.dat' ? 'text-orange-400' :
file.extension === '.op2' ? 'text-purple-400' :
'text-dark-400'
}`} />
<span className="font-medium text-white text-sm truncate" title={file.name}>
{file.name}
</span>
</div>
<span className="text-xs bg-dark-600 text-dark-400 px-2 py-0.5 rounded uppercase">
{file.extension.slice(1)}
</span>
</div>
<div className="flex items-center justify-between mt-1 text-xs text-dark-500">
<span>{file.size_display}</span>
<span>{new Date(file.modified).toLocaleDateString()}</span>
</div>
</div>
))}
</div>
) : (
<p className="text-dark-500 text-sm italic text-center py-4">
No model files found
</p>
)}
{/* File Type Legend */}
{modelFiles.length > 0 && (
<div className="pt-2 border-t border-dark-700">
<div className="flex flex-wrap gap-3 text-xs text-dark-500">
<span className="flex items-center gap-1"><span className="w-2 h-2 bg-blue-400 rounded-full"></span>.prt = Part</span>
<span className="flex items-center gap-1"><span className="w-2 h-2 bg-green-400 rounded-full"></span>.sim = Simulation</span>
<span className="flex items-center gap-1"><span className="w-2 h-2 bg-yellow-400 rounded-full"></span>.fem = FEM</span>
<span className="flex items-center gap-1"><span className="w-2 h-2 bg-orange-400 rounded-full"></span>.bdf = Nastran</span>
<span className="flex items-center gap-1"><span className="w-2 h-2 bg-purple-400 rounded-full"></span>.op2 = Results</span>
</div>
</div>
)}
</div>
)}
</Card>
{/* Extractors */}
{config?.extractors && config.extractors.length > 0 && (
<Card className="overflow-hidden">
<button
onClick={() => toggleSection('extractors')}
className="w-full flex items-center justify-between p-4 hover:bg-dark-750 transition-colors"
>
<div className="flex items-center gap-3">
<Layers className="w-5 h-5 text-primary-400" />
<h2 className="text-lg font-semibold text-white">Extractors</h2>
<span className="text-xs bg-dark-600 text-dark-300 px-2 py-0.5 rounded-full">
{config.extractors.length}
</span>
</div>
{expandedSections.has('extractors') ? (
<ChevronUp className="w-5 h-5 text-dark-400" />
) : (
<ChevronDown className="w-5 h-5 text-dark-400" />
)}
</button>
{expandedSections.has('extractors') && (
<div className="px-4 pb-4 space-y-2">
{config.extractors.map((ext, idx) => (
<div
key={idx}
className="bg-dark-750 rounded-lg p-3 border border-dark-600"
>
<div className="flex items-center justify-between">
<span className="font-medium text-white">{ext.name}</span>
<span className="text-xs bg-dark-600 text-dark-400 px-2 py-0.5 rounded">
{ext.type}
</span>
</div>
{ext.source && (
<div className="text-xs text-dark-500 mt-1 font-mono">{ext.source}</div>
)}
</div>
))}
</div>
)}
</Card>
)}
{/* Study Stats */}
<Card title="Current Progress">
<div className="grid grid-cols-2 gap-4">
<div className="bg-dark-750 rounded-lg p-4 border border-dark-600 text-center">
<div className="text-3xl font-bold text-white">
{selectedStudy.progress.current}
</div>
<div className="text-xs text-dark-400 uppercase mt-1">Trials Completed</div>
</div>
<div className="bg-dark-750 rounded-lg p-4 border border-dark-600 text-center">
<div className="text-3xl font-bold text-primary-400">
{selectedStudy.best_value?.toExponential(3) || 'N/A'}
</div>
<div className="text-xs text-dark-400 uppercase mt-1">Best Value</div>
</div>
</div>
{/* Progress Bar */}
<div className="mt-4">
<div className="flex items-center justify-between text-sm mb-2">
<span className="text-dark-400">Progress</span>
<span className="text-white">
{Math.round((selectedStudy.progress.current / selectedStudy.progress.total) * 100)}%
</span>
</div>
<div className="h-2 bg-dark-700 rounded-full overflow-hidden">
<div
className="h-full bg-primary-500 rounded-full transition-all"
style={{
width: `${Math.min((selectedStudy.progress.current / selectedStudy.progress.total) * 100, 100)}%`
}}
/>
</div>
</div>
{/* Status Badge */}
<div className="mt-4 flex items-center justify-center">
<span className={`flex items-center gap-2 px-4 py-2 rounded-full text-sm font-medium ${
selectedStudy.status === 'running' ? 'bg-green-500/10 text-green-400' :
selectedStudy.status === 'completed' ? 'bg-blue-500/10 text-blue-400' :
selectedStudy.status === 'paused' ? 'bg-orange-500/10 text-orange-400' :
'bg-dark-600 text-dark-400'
}`}>
{selectedStudy.status === 'completed' && <CheckCircle className="w-4 h-4" />}
{selectedStudy.status === 'running' && <Play className="w-4 h-4" />}
<span className="capitalize">{selectedStudy.status.replace('_', ' ')}</span>
</span>
</div>
</Card>
</div>
</div>
)}
</div>
);
}