Files
Atomizer/atomizer-dashboard/frontend/src/components/canvas/panels/ExecuteDialog.tsx

251 lines
8.8 KiB
TypeScript
Raw Normal View History

/**
* Execute Dialog - Choose to update existing study or create new one
*/
import { useState, useEffect } from 'react';
import { FilePlus, RefreshCw, FolderOpen } from 'lucide-react';
import { useStudy } from '../../../context/StudyContext';
type ExecuteMode = 'create' | 'update';
interface ExecuteDialogProps {
isOpen: boolean;
onClose: () => void;
onExecute: (studyName: string, autoRun: boolean, mode: ExecuteMode, existingStudyId?: string) => void;
isExecuting: boolean;
defaultStudyId?: string; // Pre-selected study when editing
}
export function ExecuteDialog({
isOpen,
onClose,
onExecute,
isExecuting,
defaultStudyId,
}: ExecuteDialogProps) {
const { studies } = useStudy();
const [mode, setMode] = useState<ExecuteMode>('create');
const [studyName, setStudyName] = useState('');
const [selectedStudyId, setSelectedStudyId] = useState<string>('');
const [autoRun, setAutoRun] = useState(false);
const [error, setError] = useState<string | null>(null);
// Reset state when dialog opens
useEffect(() => {
if (isOpen) {
if (defaultStudyId) {
setMode('update');
setSelectedStudyId(defaultStudyId);
} else {
setMode('create');
setSelectedStudyId(studies[0]?.id || '');
}
setStudyName('');
setAutoRun(false);
setError(null);
}
}, [isOpen, defaultStudyId, studies]);
if (!isOpen) return null;
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (mode === 'create') {
// Validate study name
const trimmed = studyName.trim();
if (!trimmed) {
setError('Study name is required');
return;
}
// Check for valid snake_case
if (!/^[a-z][a-z0-9_]*$/.test(trimmed)) {
setError('Study name must be snake_case (lowercase letters, numbers, underscores)');
return;
}
setError(null);
onExecute(trimmed, autoRun, 'create');
} else {
// Update mode
if (!selectedStudyId) {
setError('Please select a study to update');
return;
}
const selectedStudy = studies.find(s => s.id === selectedStudyId);
if (!selectedStudy) {
setError('Selected study not found');
return;
}
setError(null);
onExecute(selectedStudy.id, autoRun, 'update', selectedStudyId);
}
};
const handleClose = () => {
setStudyName('');
setAutoRun(false);
setError(null);
onClose();
};
return (
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 backdrop-blur-sm">
<div className="bg-dark-850 rounded-xl shadow-2xl w-full max-w-lg p-6 border border-dark-700">
<h2 className="text-xl font-semibold text-white mb-2">
Execute with Claude
</h2>
<p className="text-dark-400 text-sm mb-6">
Choose to create a new study or update an existing one
</p>
{/* Mode Tabs */}
<div className="flex gap-2 mb-6">
<button
type="button"
onClick={() => { setMode('create'); setError(null); }}
className={`flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-lg border transition-all ${
mode === 'create'
? 'bg-primary-600/20 border-primary-500 text-primary-400'
: 'bg-dark-800 border-dark-600 text-dark-400 hover:border-dark-500'
}`}
>
<FilePlus size={18} />
<span className="font-medium">Create New Study</span>
</button>
<button
type="button"
onClick={() => { setMode('update'); setError(null); }}
disabled={studies.length === 0}
className={`flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-lg border transition-all ${
mode === 'update'
? 'bg-amber-600/20 border-amber-500 text-amber-400'
: 'bg-dark-800 border-dark-600 text-dark-400 hover:border-dark-500'
} ${studies.length === 0 ? 'opacity-50 cursor-not-allowed' : ''}`}
>
<RefreshCw size={18} />
<span className="font-medium">Update Existing</span>
</button>
</div>
<form onSubmit={handleSubmit}>
{mode === 'create' ? (
/* Create New Study */
<div className="mb-4">
<label
htmlFor="study-name"
className="block text-sm font-medium text-dark-300 mb-2"
>
Study Name
</label>
<input
id="study-name"
type="text"
value={studyName}
onChange={(e) => setStudyName(e.target.value.toLowerCase().replace(/\s+/g, '_'))}
placeholder="my_optimization_study"
className="w-full px-3 py-2.5 bg-dark-800 border border-dark-600 text-white placeholder-dark-500 rounded-lg font-mono focus:ring-2 focus:ring-primary-500 focus:border-primary-500 focus:outline-none transition-colors"
disabled={isExecuting}
autoFocus
/>
<p className="mt-2 text-xs text-dark-500">
Use snake_case (e.g., bracket_mass_v1, mirror_wfe_optimization)
</p>
</div>
) : (
/* Update Existing Study */
<div className="mb-4">
<label
htmlFor="existing-study"
className="block text-sm font-medium text-dark-300 mb-2"
>
Select Study to Update
</label>
<div className="relative">
<FolderOpen size={18} className="absolute left-3 top-1/2 -translate-y-1/2 text-dark-500 pointer-events-none" />
<select
id="existing-study"
value={selectedStudyId}
onChange={(e) => setSelectedStudyId(e.target.value)}
className="w-full pl-10 pr-3 py-2.5 bg-dark-800 border border-dark-600 text-white rounded-lg focus:ring-2 focus:ring-amber-500 focus:border-amber-500 focus:outline-none transition-colors appearance-none cursor-pointer"
disabled={isExecuting}
>
{studies.map((study) => (
<option key={study.id} value={study.id}>
{study.name || study.id} ({study.progress.current} trials)
</option>
))}
</select>
</div>
<p className="mt-2 text-xs text-amber-500/80">
Warning: This will overwrite the study's optimization_config.json
</p>
</div>
)}
{error && (
<div className="mb-4 p-3 bg-red-500/10 border border-red-500/30 rounded-lg">
<p className="text-sm text-red-400">{error}</p>
</div>
)}
<div className="mb-6">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={autoRun}
onChange={(e) => setAutoRun(e.target.checked)}
disabled={isExecuting}
className="w-4 h-4 rounded bg-dark-800 border-dark-600 text-primary-500 focus:ring-primary-500"
/>
<span className="text-sm text-dark-300">
Start optimization immediately after {mode === 'create' ? 'creation' : 'update'}
</span>
</label>
</div>
<div className="flex gap-3 justify-end">
<button
type="button"
onClick={handleClose}
disabled={isExecuting}
className="px-4 py-2.5 text-dark-300 hover:text-white disabled:opacity-50 transition-colors"
>
Cancel
</button>
<button
type="submit"
disabled={isExecuting}
className={`px-5 py-2.5 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2 transition-colors font-medium ${
mode === 'create'
? 'bg-primary-600 text-white hover:bg-primary-500'
: 'bg-amber-600 text-white hover:bg-amber-500'
}`}
>
{isExecuting ? (
<>
<span className="animate-spin"></span>
Processing...
</>
) : mode === 'create' ? (
<>
<FilePlus size={16} />
Create & Send to Claude
</>
) : (
<>
<RefreshCw size={16} />
Update & Send to Claude
</>
)}
</button>
</div>
</form>
</div>
</div>
);
}