Datavision / frontend /src /components /automl /MultiFileUpload.tsx
DataVision CI/CD Bot
release: clean production build for HuggingFace Space
09801ca
Raw
History Blame Contribute Delete
23.4 kB
/**
* 🗂️ 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;