Dashboard: - Add Studio page with drag-drop model upload and Claude chat - Add intake system for study creation workflow - Improve session manager and context builder - Add intake API routes and frontend components Optimization Engine: - Add CLI module for command-line operations - Add intake module for study preprocessing - Add validation module with gate checks - Improve Zernike extractor documentation - Update spec models with better validation - Enhance solve_simulation robustness Documentation: - Add ATOMIZER_STUDIO.md planning doc - Add ATOMIZER_UX_SYSTEM.md for UX patterns - Update extractor library docs - Add study-readme-generator skill Tools: - Add test scripts for extraction validation - Add Zernike recentering test Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
273 lines
10 KiB
TypeScript
273 lines
10 KiB
TypeScript
/**
|
|
* FinalizeModal - Modal for finalizing an inbox study
|
|
*
|
|
* Allows user to:
|
|
* - Select/create topic folder
|
|
* - Choose whether to run baseline FEA
|
|
* - See progress during finalization
|
|
*/
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
X,
|
|
Folder,
|
|
CheckCircle,
|
|
Loader2,
|
|
AlertCircle,
|
|
} from 'lucide-react';
|
|
import { intakeApi } from '../../api/intake';
|
|
import { TopicInfo, InboxStudyDetail } from '../../types/intake';
|
|
|
|
interface FinalizeModalProps {
|
|
studyName: string;
|
|
topics: TopicInfo[];
|
|
onClose: () => void;
|
|
onFinalized: (finalPath: string) => void;
|
|
}
|
|
|
|
export const FinalizeModal: React.FC<FinalizeModalProps> = ({
|
|
studyName,
|
|
topics,
|
|
onClose,
|
|
onFinalized,
|
|
}) => {
|
|
const [studyDetail, setStudyDetail] = useState<InboxStudyDetail | null>(null);
|
|
const [selectedTopic, setSelectedTopic] = useState('');
|
|
const [newTopic, setNewTopic] = useState('');
|
|
const [runBaseline, setRunBaseline] = useState(true);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [isFinalizing, setIsFinalizing] = useState(false);
|
|
const [progress, setProgress] = useState<string>('');
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
// Load study detail
|
|
useEffect(() => {
|
|
const loadStudy = async () => {
|
|
try {
|
|
const detail = await intakeApi.getInboxStudy(studyName);
|
|
setStudyDetail(detail);
|
|
// Pre-select topic if set in spec
|
|
if (detail.spec.meta.topic) {
|
|
setSelectedTopic(detail.spec.meta.topic);
|
|
}
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to load study');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
loadStudy();
|
|
}, [studyName]);
|
|
|
|
const handleFinalize = async () => {
|
|
const topic = newTopic.trim() || selectedTopic;
|
|
if (!topic) {
|
|
setError('Please select or create a topic folder');
|
|
return;
|
|
}
|
|
|
|
setIsFinalizing(true);
|
|
setError(null);
|
|
setProgress('Starting finalization...');
|
|
|
|
try {
|
|
setProgress('Validating study configuration...');
|
|
await new Promise((r) => setTimeout(r, 500)); // Visual feedback
|
|
|
|
if (runBaseline) {
|
|
setProgress('Running baseline FEA solve...');
|
|
}
|
|
|
|
const result = await intakeApi.finalize(studyName, {
|
|
topic,
|
|
run_baseline: runBaseline,
|
|
});
|
|
|
|
setProgress('Finalization complete!');
|
|
await new Promise((r) => setTimeout(r, 500));
|
|
|
|
onFinalized(result.final_path);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Finalization failed');
|
|
setIsFinalizing(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-dark-900/80 backdrop-blur-sm">
|
|
<div className="w-full max-w-lg glass-strong rounded-xl border border-primary-400/20 overflow-hidden">
|
|
{/* Header */}
|
|
<div className="px-6 py-4 border-b border-primary-400/10 flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 rounded-lg bg-primary-400/10 flex items-center justify-center">
|
|
<Folder className="w-5 h-5 text-primary-400" />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-lg font-semibold text-white">Finalize Study</h3>
|
|
<p className="text-sm text-dark-400">{studyName}</p>
|
|
</div>
|
|
</div>
|
|
{!isFinalizing && (
|
|
<button
|
|
onClick={onClose}
|
|
className="p-2 hover:bg-white/5 rounded-lg transition-colors text-dark-400 hover:text-white"
|
|
>
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="p-6 space-y-6">
|
|
{isLoading ? (
|
|
<div className="flex items-center justify-center py-8">
|
|
<Loader2 className="w-6 h-6 animate-spin text-primary-400" />
|
|
</div>
|
|
) : isFinalizing ? (
|
|
/* Progress View */
|
|
<div className="text-center py-8 space-y-4">
|
|
<Loader2 className="w-12 h-12 animate-spin text-primary-400 mx-auto" />
|
|
<p className="text-white font-medium">{progress}</p>
|
|
<p className="text-sm text-dark-400">
|
|
Please wait while your study is being finalized...
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<>
|
|
{/* Error Display */}
|
|
{error && (
|
|
<div className="p-3 rounded-lg bg-red-500/10 border border-red-500/30 text-red-400 text-sm flex items-center gap-2">
|
|
<AlertCircle className="w-4 h-4 flex-shrink-0" />
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{/* Study Summary */}
|
|
{studyDetail && (
|
|
<div className="p-4 rounded-lg bg-dark-800 space-y-2">
|
|
<h4 className="text-sm font-medium text-dark-300">Study Summary</h4>
|
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
|
<div>
|
|
<span className="text-dark-500">Status:</span>
|
|
<span className="ml-2 text-white capitalize">
|
|
{studyDetail.spec.meta.status}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-dark-500">Model Files:</span>
|
|
<span className="ml-2 text-white">
|
|
{studyDetail.files.sim.length + studyDetail.files.prt.length + studyDetail.files.fem.length}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-dark-500">Design Variables:</span>
|
|
<span className="ml-2 text-white">
|
|
{studyDetail.spec.design_variables?.length || 0}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-dark-500">Objectives:</span>
|
|
<span className="ml-2 text-white">
|
|
{studyDetail.spec.objectives?.length || 0}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Topic Selection */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-dark-300 mb-2">
|
|
Topic Folder <span className="text-red-400">*</span>
|
|
</label>
|
|
<div className="flex gap-2">
|
|
<select
|
|
value={selectedTopic}
|
|
onChange={(e) => {
|
|
setSelectedTopic(e.target.value);
|
|
setNewTopic('');
|
|
}}
|
|
className="flex-1 px-4 py-2.5 rounded-lg bg-dark-800 border border-dark-600
|
|
text-white focus:border-primary-400 focus:outline-none
|
|
focus:ring-1 focus:ring-primary-400/50"
|
|
>
|
|
<option value="">Select existing topic...</option>
|
|
{topics.map((topic) => (
|
|
<option key={topic.name} value={topic.name}>
|
|
{topic.name} ({topic.study_count} studies)
|
|
</option>
|
|
))}
|
|
</select>
|
|
<span className="text-dark-500 self-center">or</span>
|
|
<input
|
|
type="text"
|
|
value={newTopic}
|
|
onChange={(e) => {
|
|
setNewTopic(e.target.value.replace(/[^A-Za-z0-9_]/g, '_'));
|
|
setSelectedTopic('');
|
|
}}
|
|
placeholder="New_Topic"
|
|
className="flex-1 px-4 py-2.5 rounded-lg bg-dark-800 border border-dark-600
|
|
text-white placeholder-dark-500 focus:border-primary-400
|
|
focus:outline-none focus:ring-1 focus:ring-primary-400/50"
|
|
/>
|
|
</div>
|
|
<p className="mt-1 text-xs text-dark-500">
|
|
Study will be created at: studies/{newTopic || selectedTopic || '<topic>'}/{studyName}/
|
|
</p>
|
|
</div>
|
|
|
|
{/* Baseline Option */}
|
|
<div>
|
|
<label className="flex items-center gap-3 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={runBaseline}
|
|
onChange={(e) => setRunBaseline(e.target.checked)}
|
|
className="w-4 h-4 rounded border-dark-600 bg-dark-800 text-primary-400
|
|
focus:ring-primary-400/50"
|
|
/>
|
|
<div>
|
|
<span className="text-white font-medium">Run baseline FEA solve</span>
|
|
<p className="text-xs text-dark-500">
|
|
Validates the model and captures baseline performance metrics
|
|
</p>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
{!isLoading && !isFinalizing && (
|
|
<div className="px-6 py-4 border-t border-primary-400/10 flex justify-end gap-3">
|
|
<button
|
|
onClick={onClose}
|
|
className="px-4 py-2 rounded-lg border border-dark-600 text-dark-300
|
|
hover:border-dark-500 hover:text-white transition-colors"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
onClick={handleFinalize}
|
|
disabled={!selectedTopic && !newTopic.trim()}
|
|
className="px-6 py-2 rounded-lg font-medium transition-all disabled:opacity-50
|
|
flex items-center gap-2"
|
|
style={{
|
|
background: 'linear-gradient(135deg, #00d4e6 0%, #0891b2 100%)',
|
|
color: '#000',
|
|
}}
|
|
>
|
|
<CheckCircle className="w-4 h-4" />
|
|
Finalize Study
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default FinalizeModal;
|