Spaces:
Running
Running
| /** | |
| * 🗂️ 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<MultiFileUploadProps> = ({ onTrainingComplete, ultraMode: parentUltraMode, onUltraModeChange }) => { | |
| const [trainFile, setTrainFile] = useState<FileInfo>({ file: null, name: '', rows: null, columns: null, error: null }); | |
| const [testFile, setTestFile] = useState<FileInfo>({ file: null, name: '', rows: null, columns: null, error: null }); | |
| const [targetColumn, setTargetColumn] = useState<string>(''); | |
| const [columns, setColumns] = useState<string[]>([]); | |
| const [isTraining, setIsTraining] = useState(false); | |
| const [trainingProgress, setTrainingProgress] = useState(''); | |
| const [error, setError] = useState<string | null>(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<AbortController | null>(null); | |
| const trainInputRef = useRef<HTMLInputElement>(null); | |
| const testInputRef = useRef<HTMLInputElement>(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<HTMLDivElement>) => { | |
| 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<HTMLDivElement>) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| if (type === 'train') setTrainDragActive(true); | |
| else setTestDragActive(true); | |
| }; | |
| const handleDragLeave = (type: 'train' | 'test', e: React.DragEvent<HTMLDivElement>) => { | |
| 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<HTMLInputElement>; | |
| }) => { | |
| const isDragActive = type === 'train' ? trainDragActive : testDragActive; | |
| return ( | |
| <div | |
| onClick={() => 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)' | |
| }} | |
| > | |
| <input | |
| ref={inputRef} | |
| type="file" | |
| accept=".csv,.xlsx,.xls" | |
| className="hidden" | |
| onChange={(e) => handleFileSelect(type, e.target.files?.[0] || null)} | |
| /> | |
| <div className="flex flex-col items-center gap-3"> | |
| {fileInfo.file ? ( | |
| <> | |
| <div className="p-3 rounded-xl bg-emerald-500/20"> | |
| <FileSpreadsheet className="w-8 h-8 text-emerald-400" /> | |
| </div> | |
| <div className="text-center"> | |
| <p className="font-medium" style={{ color: 'var(--text-primary)' }}>{fileInfo.name}</p> | |
| <p className="text-sm" style={{ color: 'var(--text-muted)' }}> | |
| {fileInfo.rows?.toLocaleString()} rows × {fileInfo.columns} columns | |
| </p> | |
| </div> | |
| {fileInfo.error && ( | |
| <div className="flex items-center gap-2 text-red-400 text-sm"> | |
| <AlertTriangle className="w-4 h-4" /> | |
| {fileInfo.error} | |
| </div> | |
| )} | |
| </> | |
| ) : ( | |
| <> | |
| <div className={`p-3 rounded-xl transition-all ${isDragActive ? 'bg-emerald-500/20' : ''}`} style={{ backgroundColor: !isDragActive ? 'var(--bg-secondary)' : undefined }}> | |
| <Upload className={`w-8 h-8 ${isDragActive ? 'text-emerald-400' : ''}`} style={{ color: isDragActive ? '#10b981' : 'var(--text-muted)' }} /> | |
| </div> | |
| <div className="text-center"> | |
| <p className="font-medium" style={{ color: isDragActive ? '#10b981' : 'var(--text-primary)' }}> | |
| {isDragActive ? 'Drop here!' : `Drop ${type === 'train' ? 'Training' : 'Test'} File`} | |
| </p> | |
| <p className="text-sm" style={{ color: 'var(--text-muted)' }}> | |
| CSV or Excel | |
| </p> | |
| </div> | |
| </> | |
| )} | |
| </div> | |
| {/* Label Badge */} | |
| <div className={`absolute top-3 left-3 px-2 py-1 rounded-full text-xs font-medium ${type === 'train' ? 'bg-blue-500/20 text-blue-400' : 'bg-purple-500/20 text-purple-400' | |
| }`}> | |
| {type === 'train' ? '📚 Train' : '🧪 Test'} | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| return ( | |
| <motion.div | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| className="p-6 rounded-2xl border" | |
| style={{ backgroundColor: 'var(--bg-card)', borderColor: 'var(--border-color)' }} | |
| > | |
| <h3 className="text-lg font-semibold mb-4 flex items-center gap-2" style={{ color: 'var(--text-primary)' }}> | |
| <FileSpreadsheet className="w-5 h-5 text-emerald-400" /> | |
| Multi-File Training | |
| <span className="ml-2 px-2 py-0.5 rounded-full bg-blue-500/20 text-blue-400 text-xs"> | |
| Separate Train/Test | |
| </span> | |
| </h3> | |
| <div className="flex items-center gap-2 mb-6 p-3 rounded-xl bg-blue-500/10 border border-blue-500/20"> | |
| <Info className="w-5 h-5 text-blue-400 flex-shrink-0" /> | |
| <p className="text-sm" style={{ color: 'var(--text-muted)' }}> | |
| Use separate train and test files for unbiased model evaluation. The test set will NOT be used for training. | |
| </p> | |
| </div> | |
| {/* File Upload Areas */} | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6"> | |
| <FileDropZone type="train" fileInfo={trainFile} inputRef={trainInputRef} /> | |
| <FileDropZone type="test" fileInfo={testFile} inputRef={testInputRef} /> | |
| </div> | |
| {/* Arrow indicator */} | |
| {trainFile.file && testFile.file && ( | |
| <div className="flex items-center justify-center gap-4 mb-6"> | |
| <div className="flex-1 h-px bg-gradient-to-r from-transparent via-gray-500 to-transparent" /> | |
| <ArrowRight className="w-6 h-6 text-emerald-400" /> | |
| <div className="flex-1 h-px bg-gradient-to-r from-transparent via-gray-500 to-transparent" /> | |
| </div> | |
| )} | |
| {/* Target Column Selection */} | |
| {columns.length > 0 && ( | |
| <div className="mb-6"> | |
| <label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-muted)' }}> | |
| Target Column | |
| </label> | |
| <select | |
| value={targetColumn} | |
| onChange={(e) => setTargetColumn(e.target.value)} | |
| className="w-full p-3 rounded-xl border bg-transparent outline-none focus:border-emerald-500" | |
| style={{ borderColor: 'var(--border-color)', color: 'var(--text-primary)' }} | |
| > | |
| {columns.map((col) => ( | |
| <option key={col} value={col} style={{ backgroundColor: 'var(--bg-card)' }}> | |
| {col} | |
| </option> | |
| ))} | |
| </select> | |
| </div> | |
| )} | |
| {/* Fast/Ultra Mode Toggle */} | |
| {columns.length > 0 && ( | |
| <div className="mb-6 flex items-center gap-4"> | |
| <button | |
| onClick={() => setUltraMode(false)} | |
| disabled={isTraining} | |
| className={`flex-1 p-4 rounded-xl border-2 transition-all ${!ultraMode | |
| ? 'border-emerald-500 bg-emerald-500/10' | |
| : 'border-transparent hover:border-gray-500/30' | |
| }`} | |
| style={{ backgroundColor: !ultraMode ? 'rgba(16, 185, 129, 0.1)' : 'var(--bg-card)' }} | |
| > | |
| <div className="flex items-center gap-3"> | |
| <Zap className={`w-6 h-6 ${!ultraMode ? 'text-emerald-400' : 'text-gray-500'}`} /> | |
| <div className="text-left"> | |
| <p className="font-semibold" style={{ color: !ultraMode ? '#10b981' : 'var(--text-primary)' }}> | |
| ⚡ Fast Mode | |
| </p> | |
| <p className="text-xs" style={{ color: 'var(--text-muted)' }}> | |
| 1-2 min • 7 models | |
| </p> | |
| </div> | |
| </div> | |
| </button> | |
| <button | |
| onClick={() => setUltraMode(true)} | |
| disabled={isTraining} | |
| className={`flex-1 p-4 rounded-xl border-2 transition-all ${ultraMode | |
| ? 'border-purple-500 bg-purple-500/10' | |
| : 'border-transparent hover:border-gray-500/30' | |
| }`} | |
| style={{ backgroundColor: ultraMode ? 'rgba(139, 92, 246, 0.1)' : 'var(--bg-card)' }} | |
| > | |
| <div className="flex items-center gap-3"> | |
| <Brain className={`w-6 h-6 ${ultraMode ? 'text-purple-400' : 'text-gray-500'}`} /> | |
| <div className="text-left"> | |
| <p className="font-semibold" style={{ color: ultraMode ? '#a78bfa' : 'var(--text-primary)' }}> | |
| 🚀 Ultra Mode | |
| </p> | |
| <p className="text-xs" style={{ color: 'var(--text-muted)' }}> | |
| 5-10 min • 20+ models | |
| </p> | |
| </div> | |
| </div> | |
| </button> | |
| </div> | |
| )} | |
| {/* Error Display */} | |
| {error && ( | |
| <div className="flex items-center gap-2 p-3 mb-4 rounded-xl bg-red-500/10 border border-red-500/20"> | |
| <XCircle className="w-5 h-5 text-red-400" /> | |
| <p className="text-sm text-red-400">{error}</p> | |
| </div> | |
| )} | |
| {/* Train Button */} | |
| <div className="flex gap-3"> | |
| <button | |
| onClick={handleTrain} | |
| disabled={!trainFile.file || !testFile.file || isTraining} | |
| className={`flex-1 py-4 rounded-xl font-semibold flex items-center justify-center gap-3 transition-all ${trainFile.file && testFile.file && !isTraining | |
| ? 'bg-gradient-to-r from-emerald-500 to-green-500 text-white hover:opacity-90' | |
| : 'bg-gray-500/20 text-gray-500 cursor-not-allowed' | |
| }`} | |
| > | |
| {isTraining ? ( | |
| <> | |
| <Loader className="w-5 h-5 animate-spin" /> | |
| {trainingProgress} | |
| </> | |
| ) : ( | |
| <> | |
| <CheckCircle className="w-5 h-5" /> | |
| Train with Separate Test Set | |
| </> | |
| )} | |
| </button> | |
| {/* Stop Training Button - Only visible during training */} | |
| {isTraining && ( | |
| <button | |
| onClick={handleStopTraining} | |
| className="px-6 py-4 rounded-xl font-semibold bg-red-500/10 border border-red-500/30 text-red-400 hover:bg-red-500/20 transition-all flex items-center gap-2" | |
| > | |
| <XCircle className="w-5 h-5" /> | |
| Stop | |
| </button> | |
| )} | |
| </div> | |
| {/* Summary Stats */} | |
| {trainFile.file && testFile.file && ( | |
| <div className="mt-4 grid grid-cols-3 gap-4 text-center"> | |
| <div> | |
| <p className="text-2xl font-bold text-blue-400">{trainFile.rows?.toLocaleString()}</p> | |
| <p className="text-xs" style={{ color: 'var(--text-muted)' }}>Training Samples</p> | |
| </div> | |
| <div> | |
| <p className="text-2xl font-bold text-purple-400">{testFile.rows?.toLocaleString()}</p> | |
| <p className="text-xs" style={{ color: 'var(--text-muted)' }}>Test Samples</p> | |
| </div> | |
| <div> | |
| <p className="text-2xl font-bold text-emerald-400">{trainFile.columns}</p> | |
| <p className="text-xs" style={{ color: 'var(--text-muted)' }}>Features</p> | |
| </div> | |
| </div> | |
| )} | |
| </motion.div> | |
| ); | |
| }; | |
| export default MultiFileUpload; | |