import React, { useState, useEffect } from 'react'; import { X, AlertCircle, Calendar, Settings2, Info, Layers, Repeat } from 'lucide-react'; import { SearchableSelect } from './SearchableSelect'; interface ScheduleModalProps { isOpen: boolean; onClose: () => void; onSave: (data: any) => void; initialData: { task: string; metrics: string[]; modelType?: string; dataset?: { columns: Array<{ name: string }>; }; retraining_job?: { frequency: string; tuning_frequency: string; at: { hour: number; day_of_week?: number; day_of_month?: number; } direction: string; batch_mode?: boolean; batch_size?: number; batch_overlap?: number; batch_key?: string; active: boolean; metric?: string; threshold?: number; tuner_config?: { n_trials: number; objective: string; config: Record; }; tuning_enabled?: boolean; }; }; tunerJobConstants: any; timezone: string; retrainingJobConstants: any; } const METRICS = { classification: [ { value: 'accuracy_score', label: 'Accuracy', description: 'Overall prediction accuracy' }, { value: 'precision_score', label: 'Precision', description: 'Ratio of true positives to predicted positives' }, { value: 'recall_score', label: 'Recall', description: 'Ratio of true positives to actual positives' }, { value: 'f1_score', label: 'F1 Score', description: 'Harmonic mean of precision and recall' } ], regression: [ { value: 'mean_absolute_error', label: 'Mean Absolute Error', description: 'Average absolute differences between predicted and actual values' }, { value: 'mean_squared_error', label: 'Mean Squared Error', description: 'Average squared differences between predicted and actual values' }, { value: 'root_mean_squared_error', label: 'Root Mean Squared Error', description: 'Square root of mean squared error' }, { value: 'r2_score', label: 'R² Score', description: 'Proportion of variance in the target that is predictable' } ] }; export function ScheduleModal({ isOpen, onClose, onSave, initialData, tunerJobConstants, timezone, retrainingJobConstants }: ScheduleModalProps) { const [showBatchTrainingInfo, setShowBatchTrainingInfo] = useState(false); const [activeBatchPopover, setActiveBatchPopover] = useState<'size' | 'overlap' | null>(null); // Get all base parameters (those with options) const baseParameters = Object.entries(tunerJobConstants) .filter(([_, value]) => Array.isArray(value.options)) .reduce((acc, [key, value]) => ({ ...acc, [key]: value.options[0].value // Default to first option if not set }), {}); // Get default numeric parameters for the default booster const defaultBooster = baseParameters.booster; const defaultNumericParameters = Object.entries(tunerJobConstants.hyperparameters[defaultBooster] || {}) .filter(([_, value]) => !Array.isArray(value.options)) .reduce((acc, [key, value]) => ({ ...acc, [key]: { min: value.min, max: value.max } }), {}); const [formData, setFormData] = useState({ retraining_job_attributes: { id: initialData.retraining_job?.id || null, active: initialData.retraining_job?.active ?? false, frequency: initialData.retraining_job?.frequency || retrainingJobConstants.frequency[0].value as string, tuning_frequency: initialData.retraining_job?.tuning_frequency || 'month', direction: initialData.retraining_job?.direction || 'maximize', batch_mode: initialData.retraining_job?.batch_mode || false, batch_size: initialData.retraining_job?.batch_size || 100, batch_overlap: initialData.retraining_job?.batch_overlap || 1, batch_key: initialData.retraining_job?.batch_key || '', at: { hour: initialData.retraining_job?.at?.hour ?? 2, day_of_week: initialData.retraining_job?.at?.day_of_week ?? 1, day_of_month: initialData.retraining_job?.at?.day_of_month ?? 1 }, metric: initialData.retraining_job?.metric || METRICS[initialData.task === 'classification' ? 'classification' : 'regression'][0].value, threshold: initialData.retraining_job?.threshold || (initialData.task === 'classification' ? 0.85 : 0.1), tuner_config: initialData.retraining_job?.tuner_config ? { n_trials: initialData.retraining_job.tuner_config.n_trials || 10, config: { ...baseParameters, ...defaultNumericParameters, ...initialData.retraining_job.tuner_config.config } } : undefined, tuning_enabled: initialData.retraining_job?.tuning_enabled ?? false } }); useEffect(() => { if (formData.retraining_job_attributes.tuner_config && Object.keys(formData.retraining_job_attributes.tuner_config.config).length === 0) { setFormData(prev => ({ ...prev, retraining_job_attributes: { ...prev.retraining_job_attributes, tuner_config: { ...prev.retraining_job_attributes.tuner_config, config: { ...baseParameters, ...defaultNumericParameters } } } })); } }, [formData.retraining_job_attributes.tuner_config]); if (!isOpen) return null; const handleBaseParameterChange = (parameter: string, value: string) => { setFormData(prev => ({ ...prev, retraining_job_attributes: { ...prev.retraining_job_attributes, tuner_config: { ...prev.retraining_job_attributes.tuner_config, config: { ...prev.retraining_job_attributes.tuner_config.config, [parameter]: value } } } })); }; const renderHyperparameterControls = () => { const baseParameters = Object.entries(tunerJobConstants).filter( ([key, value]) => Array.isArray(value.options) ); // Include all base parameters, not just those in config const selectedBaseParams = baseParameters.map(([key]) => key); return (
{baseParameters.map(([key, value]) => (
({ value: option.value, label: option.label, description: option.description }))} value={formData.retraining_job_attributes.tuner_config?.config[key] || value.options[0].value} onChange={(val) => handleBaseParameterChange(key, val as string)} />
))} {selectedBaseParams.map(param => { const subParams = Object.entries(tunerJobConstants).filter( ([key, value]) => value.depends_on === param ); const selectedValue = formData.retraining_job_attributes.tuner_config?.config[param] || tunerJobConstants[param].options[0].value; // Use default if not in config return (

Parameter Ranges

{subParams.map(([subKey, subValue]: any) => { const relevantParams = subValue[selectedValue]; if (!relevantParams) return null; return Object.entries(relevantParams).map(([paramKey, paramValue]: any) => { if (paramValue.min !== undefined && paramValue.max !== undefined) { return (
{paramValue.description}
handleParameterChange( paramKey, 'min', parseFloat(e.target.value) )} className="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500" />
handleParameterChange( paramKey, 'max', parseFloat(e.target.value) )} className="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500" />
); } return null; }); })}
); })}
); }; const handleParameterChange = (parameter: string, type: 'min' | 'max', value: number) => { setFormData(prev => ({ ...prev, retraining_job_attributes: { ...prev.retraining_job_attributes, tuner_config: { ...prev.retraining_job_attributes.tuner_config, config: { ...prev.retraining_job_attributes.tuner_config.config, [parameter]: { ...prev.retraining_job_attributes.tuner_config.config[parameter], [type]: value } } } } })); }; const handleTrainingScheduleChange = (field: string, value: string | number) => { setFormData(prev => { if (field === 'hour' || field === 'day_of_week' || field === 'day_of_month') { return { ...prev, retraining_job_attributes: { ...prev.retraining_job_attributes, at: { ...prev.retraining_job_attributes.at, [field]: value } } }; } return { ...prev, retraining_job_attributes: { ...prev.retraining_job_attributes, [field]: value } }; }); }; const handleEvaluatorChange = (field: string, value: string | number) => { setFormData(prev => ({ ...prev, retraining_job_attributes: { ...prev.retraining_job_attributes, [field]: value } })); }; const handleSave = () => { const { retraining_job_attributes } = formData; const at: any = { hour: retraining_job_attributes.at.hour }; // Only include relevant date attributes based on frequency switch (retraining_job_attributes.frequency) { case 'day': // For daily frequency, only include hour break; case 'week': // For weekly frequency, include hour and day_of_week at.day_of_week = retraining_job_attributes.at.day_of_week; break; case 'month': // For monthly frequency, include hour and day_of_month at.day_of_month = retraining_job_attributes.at.day_of_month; break; } const updatedData = { retraining_job_attributes: { ...retraining_job_attributes, at } }; onSave(updatedData); onClose(); }; return (

Training Configuration

{/* Left Column */}
{/* Training Schedule */}

Training Schedule

setFormData(prev => ({ ...prev, retraining_job_attributes: { ...prev.retraining_job_attributes, active: e.target.checked } }))} className="rounded border-gray-300 text-blue-600 focus:ring-blue-500" />
{!formData.retraining_job_attributes.active && (

Manual Training Mode

The model will only be trained when you manually trigger training. You can do this from the model details page at any time.

)} {formData.retraining_job_attributes.active && ( <>
({ value: freq.value, label: freq.label, description: freq.description }))} value={formData.retraining_job_attributes.frequency} onChange={(value) => handleTrainingScheduleChange('frequency', value)} />
{formData.retraining_job_attributes.frequency === 'week' && (
handleTrainingScheduleChange('day_of_week', value)} />
)} {formData.retraining_job_attributes.frequency === 'month' && (
({ value: i + 1, label: `Day ${i + 1}` }))} value={formData.retraining_job_attributes.at.day_of_month} onChange={(value) => handleTrainingScheduleChange('day_of_month', value)} />
)}
({ value: i, label: `${i}:00` }))} value={formData.retraining_job_attributes.at.hour} onChange={(value) => handleTrainingScheduleChange('hour', value)} />
)} {/* Batch Training Options */}
setFormData({ ...formData, retraining_job_attributes: { ...formData.retraining_job_attributes, batch_mode: e.target.checked } })} className="rounded border-gray-300 text-blue-600 focus:ring-blue-500" />
{showBatchTrainingInfo && (
  • • When disabled, the model will train on the entire dataset in a single pass.
  • • When enabled, the model will learn from small batches of data iteratively, improving training speed
)} {formData.retraining_job_attributes.batch_mode && (
setFormData({ ...formData, retraining_job_attributes: { ...formData.retraining_job_attributes, batch_size: parseInt(e.target.value) } })} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" />
setFormData({ ...formData, retraining_job_attributes: { ...formData.retraining_job_attributes, batch_overlap: parseInt(e.target.value) } })} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" />
setFormData({ ...formData, retraining_job_attributes: { ...formData.retraining_job_attributes, batch_key: value } })} options={initialData.dataset?.columns?.map(column => ({ value: column.name, label: column.name })) || []} placeholder="Select a column for batch key" />
)}
{/* Evaluator Configuration */}

Evaluator Configuration

({ value: metric.value, label: metric.label, description: metric.description }))} value={formData.retraining_job_attributes.metric} onChange={(value) => handleEvaluatorChange('metric', value)} />
handleEvaluatorChange('threshold', parseFloat(e.target.value))} step={0.01} min={0} max={1} className="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 py-2 px-4 shadow-sm border-gray-300 border" />
{/* Deployment Criteria */}

Deployment Criteria

The model will be automatically deployed when the {formData.retraining_job_attributes.metric} is{' '} {formData.retraining_job_attributes.direction === 'minimize' ? 'below' : 'above'} {formData.retraining_job_attributes.threshold}.

{/* Right Column */}

Hyperparameter Tuning

setFormData(prev => ({ ...prev, retraining_job_attributes: { ...prev.retraining_job_attributes, tuning_enabled: e.target.checked, tuner_config: e.target.checked ? { n_trials: 10, config: defaultNumericParameters } : undefined } }))} className="rounded border-gray-300 text-blue-600 focus:ring-blue-500" />
{formData.retraining_job_attributes.tuning_enabled && (
setFormData(prev => ({ ...prev, retraining_job_attributes: { ...prev.retraining_job_attributes, tuning_frequency: value as 'week' | 'month' | 'always' } }))} />
setFormData(prev => ({ ...prev, retraining_job_attributes: { ...prev.retraining_job_attributes, tuner_config: { ...prev.retraining_job_attributes.tuner_config, n_trials: parseInt(e.target.value) } } }))} className="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 py-2 px-4 shadow-sm border-gray-300 border" />
{renderHyperparameterControls()}
)}
); }