/** * ๐Ÿ—‚๏ธ Multi-File Training Component * * Allows users to upload separate train and test files for ML training. * Connects to the /api/v1/automl/train_with_test endpoint. */ import React, { useState, useRef } from 'react'; import { motion } from 'framer-motion'; import { Upload, FileSpreadsheet, CheckCircle, XCircle, Loader, ArrowRight, Info, AlertTriangle, Zap, Brain, } from 'lucide-react'; import apiService from '@/services/api'; import { getUserIdSync } from '@/utils/userId'; // Theme interface removed interface MultiFileUploadProps { onTrainingComplete: (result: any) => void; ultraMode?: boolean; onUltraModeChange?: (mode: boolean) => void; } interface FileInfo { file: File | null; name: string; rows: number | null; columns: number | null; error: string | null; } const MultiFileUpload: React.FC = ({ onTrainingComplete, ultraMode: parentUltraMode, onUltraModeChange }) => { const [trainFile, setTrainFile] = useState({ file: null, name: '', rows: null, columns: null, error: null }); const [testFile, setTestFile] = useState({ file: null, name: '', rows: null, columns: null, error: null }); const [targetColumn, setTargetColumn] = useState(''); const [columns, setColumns] = useState([]); const [isTraining, setIsTraining] = useState(false); const [trainingProgress, setTrainingProgress] = useState(''); const [error, setError] = useState(null); // Use parent's ultraMode if provided, otherwise use local state const [localUltraMode, setLocalUltraMode] = useState(false); const ultraMode = parentUltraMode !== undefined ? parentUltraMode : localUltraMode; const setUltraMode = (mode: boolean) => { if (onUltraModeChange) { onUltraModeChange(mode); } else { setLocalUltraMode(mode); } }; // AbortController for stopping training const [abortController, setAbortController] = useState(null); const trainInputRef = useRef(null); const testInputRef = useRef(null); // Parse CSV to get row/column count const parseCSV = (file: File): Promise<{ rows: number; columns: string[] }> => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => { const text = e.target?.result as string; const lines = text.split('\n').filter(line => line.trim()); const headers = lines[0].split(',').map(h => h.trim().replace(/"/g, '')); resolve({ rows: lines.length - 1, columns: headers }); }; reader.onerror = () => reject(new Error('Failed to read file')); reader.readAsText(file); }); }; const handleFileSelect = async (type: 'train' | 'test', file: File | null) => { if (!file) return; const setter = type === 'train' ? setTrainFile : setTestFile; try { const parsed = await parseCSV(file); setter({ file, name: file.name, rows: parsed.rows, columns: parsed.columns.length, error: null }); // Set columns from train file for target selection if (type === 'train') { setColumns(parsed.columns); // Auto-select last column as target setTargetColumn(parsed.columns[parsed.columns.length - 1]); } // Validate test file has same columns if (type === 'test' && trainFile.file) { const trainParsed = await parseCSV(trainFile.file); if (parsed.columns.length !== trainParsed.columns.length) { setter(prev => ({ ...prev, error: `Column count mismatch: Train=${trainParsed.columns.length}, Test=${parsed.columns.length}` })); } } // Immediately upload file to DataHub for persistence try { await apiService.uploadFiles([file]); // Notify DataHub to refresh file list window.dispatchEvent(new CustomEvent('filesUpdated')); console.log(`โœ… ${file.name} uploaded to DataHub`); } catch (uploadErr) { console.warn('File upload to DataHub failed:', uploadErr); } } catch (err) { setter({ file: null, name: file.name, rows: null, columns: null, error: 'Failed to parse file' }); } }; const handleTrain = async () => { if (!trainFile.file || !testFile.file) { setError('Please upload both train and test files'); return; } setIsTraining(true); setError(null); setTrainingProgress('Starting training...'); try { const userId = getUserIdSync(); // Files are already uploaded to DataHub on selection // Just proceed to training // Step 2: Train with the training file const trainFormData = new FormData(); trainFormData.append('file', trainFile.file); trainFormData.append('user_id', userId); if (targetColumn) { trainFormData.append('target_column', targetColumn); } // Use Ultra or Fast endpoint based on mode const endpoint = ultraMode ? '/api/v2/automl/ultra_train' : '/api/v2/automl/train'; const modeLabel = ultraMode ? 'Ultra' : 'Fast'; if (ultraMode) { trainFormData.append('mode', 'maximum_accuracy'); setTrainingProgress(`๐Ÿš€ ${modeLabel} Training (5-10 minutes)...`); } else { setTrainingProgress(`โšก ${modeLabel} Training (1-2 minutes)...`); } // Create AbortController for this request const controller = new AbortController(); setAbortController(controller); const response = await fetch(endpoint, { method: 'POST', body: trainFormData, signal: controller.signal, }); setTrainingProgress('Processing results...'); // Check response status first if (!response.ok) { const errorText = await response.text(); throw new Error(`HTTP ${response.status}: ${errorText.slice(0, 100)}`); } let result; try { result = await response.json(); } catch (parseErr) { throw new Error('Failed to parse server response'); } if (result.success) { setTrainingProgress(`Complete! โœ… (${modeLabel} Mode)`); // Save to localStorage like DataHub does (for dashboard/charts) try { localStorage.setItem(`mlResults_${userId}`, JSON.stringify(result)); localStorage.setItem(`hasMLResults_${userId}`, 'true'); } catch (e) { console.warn('Results too large for localStorage, saving without charts'); const { charts, ...lightResult } = result; localStorage.setItem(`mlResults_${userId}`, JSON.stringify(lightResult)); localStorage.setItem(`hasMLResults_${userId}`, 'true'); } // Save charts if available if (result.charts) { try { sessionStorage.setItem(`mlCharts_${userId}`, JSON.stringify(result.charts)); } catch (e) { console.warn('Charts too large for sessionStorage'); } } // Dispatch event so dashboard updates window.dispatchEvent(new CustomEvent('filesUpdated')); // Small delay to show success before navigating setTimeout(() => { onTrainingComplete(result); }, 500); } else { setError(result.detail || result.error || 'Training failed'); } } catch (err: any) { if (err.name === 'AbortError') { setError('Training stopped by user'); } else { console.error('Training error:', err); setError(err.message || 'Failed to connect to server'); } } finally { setIsTraining(false); setAbortController(null); } }; // Stop training handler const handleStopTraining = async () => { if (abortController) { abortController.abort(); } setIsTraining(false); setAbortController(null); setTrainingProgress(''); // Signal backend to stop try { const userId = getUserIdSync(); const formData = new FormData(); formData.append('user_id', userId); await fetch('/api/v2/automl/stop_training', { method: 'POST', body: formData }); } catch (e) { console.error('Failed to signal stop to backend', e); } }; const [trainDragActive, setTrainDragActive] = useState(false); const [testDragActive, setTestDragActive] = useState(false); const handleDrop = (type: 'train' | 'test', e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); if (type === 'train') setTrainDragActive(false); else setTestDragActive(false); const files = e.dataTransfer.files; if (files && files.length > 0) { handleFileSelect(type, files[0]); } }; const handleDragOver = (type: 'train' | 'test', e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); if (type === 'train') setTrainDragActive(true); else setTestDragActive(true); }; const handleDragLeave = (type: 'train' | 'test', e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); if (type === 'train') setTrainDragActive(false); else setTestDragActive(false); }; const FileDropZone = ({ type, fileInfo, inputRef }: { type: 'train' | 'test'; fileInfo: FileInfo; inputRef: React.RefObject; }) => { const isDragActive = type === 'train' ? trainDragActive : testDragActive; return (
inputRef.current?.click()} onDrop={(e) => handleDrop(type, e)} onDragOver={(e) => handleDragOver(type, e)} onDragLeave={(e) => handleDragLeave(type, e)} className={`relative p-6 border-2 border-dashed rounded-2xl cursor-pointer transition-all ${isDragActive ? 'border-emerald-500 bg-emerald-500/10 scale-105' : fileInfo.file ? 'border-emerald-500/30 bg-emerald-500/5' : 'hover:border-emerald-500/50' }`} style={{ borderColor: isDragActive ? '#10b981' : fileInfo.file ? 'rgba(16, 185, 129, 0.3)' : 'var(--border-color)', transform: isDragActive ? 'scale(1.02)' : 'scale(1)' }} > handleFileSelect(type, e.target.files?.[0] || null)} />
{fileInfo.file ? ( <>

{fileInfo.name}

{fileInfo.rows?.toLocaleString()} rows ร— {fileInfo.columns} columns

{fileInfo.error && (
{fileInfo.error}
)} ) : ( <>

{isDragActive ? 'Drop here!' : `Drop ${type === 'train' ? 'Training' : 'Test'} File`}

CSV or Excel

)}
{/* Label Badge */}
{type === 'train' ? '๐Ÿ“š Train' : '๐Ÿงช Test'}
); }; return (

Multi-File Training Separate Train/Test

Use separate train and test files for unbiased model evaluation. The test set will NOT be used for training.

{/* File Upload Areas */}
{/* Arrow indicator */} {trainFile.file && testFile.file && (
)} {/* Target Column Selection */} {columns.length > 0 && (
)} {/* Fast/Ultra Mode Toggle */} {columns.length > 0 && (
)} {/* Error Display */} {error && (

{error}

)} {/* Train Button */}
{/* Stop Training Button - Only visible during training */} {isTraining && ( )}
{/* Summary Stats */} {trainFile.file && testFile.file && (

{trainFile.rows?.toLocaleString()}

Training Samples

{testFile.rows?.toLocaleString()}

Test Samples

{trainFile.columns}

Features

)} ); }; export default MultiFileUpload;