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
872 lines
36 KiB
TypeScript
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>
|
|
);
|
|
}
|