2025-08-04 18:21:42 +02:00
|
|
|
import React, { useState, useEffect } from 'react';
|
|
|
|
|
import {
|
|
|
|
|
Brain, Cpu, Database, TrendingUp, CheckCircle, AlertCircle,
|
|
|
|
|
Clock, Zap, Target, BarChart3, Loader
|
|
|
|
|
} from 'lucide-react';
|
|
|
|
|
|
|
|
|
|
interface TrainingProgressProps {
|
|
|
|
|
progress: {
|
|
|
|
|
progress: number;
|
|
|
|
|
status: string;
|
|
|
|
|
currentStep: string;
|
|
|
|
|
productsCompleted: number;
|
|
|
|
|
productsTotal: number;
|
|
|
|
|
estimatedTimeRemaining: number;
|
|
|
|
|
error?: string;
|
|
|
|
|
};
|
|
|
|
|
onTimeout?: () => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Map backend steps to user-friendly information
|
|
|
|
|
const STEP_INFO_MAP = {
|
|
|
|
|
'data_validation': {
|
|
|
|
|
title: 'Validando tus datos',
|
|
|
|
|
description: 'Verificamos la calidad y completitud de tu información histórica',
|
|
|
|
|
tip: '💡 Datos más completos = predicciones más precisas',
|
|
|
|
|
icon: Database,
|
|
|
|
|
color: 'blue'
|
|
|
|
|
},
|
|
|
|
|
'feature_engineering': {
|
|
|
|
|
title: 'Creando características predictivas',
|
|
|
|
|
description: 'Identificamos patrones estacionales y tendencias en tus ventas',
|
|
|
|
|
tip: '💡 Tu modelo detectará automáticamente picos de demanda',
|
|
|
|
|
icon: TrendingUp,
|
|
|
|
|
color: 'indigo'
|
|
|
|
|
},
|
|
|
|
|
'model_training': {
|
|
|
|
|
title: 'Entrenando modelo de IA',
|
|
|
|
|
description: 'Creamos tu modelo personalizado usando algoritmos avanzados',
|
|
|
|
|
tip: '💡 Este proceso optimiza las predicciones para tu negocio específico',
|
|
|
|
|
icon: Brain,
|
|
|
|
|
color: 'purple'
|
|
|
|
|
},
|
|
|
|
|
'model_validation': {
|
|
|
|
|
title: 'Validando precisión',
|
|
|
|
|
description: 'Verificamos que el modelo genere predicciones confiables',
|
|
|
|
|
tip: '💡 Garantizamos que las predicciones sean útiles para tu toma de decisiones',
|
|
|
|
|
icon: Target,
|
|
|
|
|
color: 'green'
|
|
|
|
|
},
|
2025-08-04 18:58:12 +02:00
|
|
|
// Handle any unmapped steps
|
2025-08-04 18:21:42 +02:00
|
|
|
'default': {
|
|
|
|
|
title: 'Procesando...',
|
|
|
|
|
description: 'Procesando tus datos para crear el modelo de predicción',
|
|
|
|
|
tip: '💡 Cada paso nos acerca a predicciones más precisas',
|
|
|
|
|
icon: Cpu,
|
|
|
|
|
color: 'gray'
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const EXPECTED_BENEFITS = [
|
|
|
|
|
{
|
|
|
|
|
icon: BarChart3,
|
|
|
|
|
title: 'Predicciones Precisas',
|
|
|
|
|
description: 'Conoce exactamente cuánto vender cada día'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
icon: Zap,
|
|
|
|
|
title: 'Optimización Automática',
|
|
|
|
|
description: 'Reduce desperdicios y maximiza ganancias'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
icon: TrendingUp,
|
|
|
|
|
title: 'Detección de Tendencias',
|
|
|
|
|
description: 'Identifica patrones estacionales y eventos especiales'
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
export default function EnhancedTrainingProgress({ progress, onTimeout }: TrainingProgressProps) {
|
|
|
|
|
const [showTimeoutWarning, setShowTimeoutWarning] = useState(false);
|
|
|
|
|
const [startTime] = useState(Date.now());
|
|
|
|
|
|
|
|
|
|
// Auto-show timeout warning after 8 minutes (480,000ms)
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const timeoutTimer = setTimeout(() => {
|
|
|
|
|
if (progress.status === 'running' && progress.progress < 100) {
|
|
|
|
|
setShowTimeoutWarning(true);
|
|
|
|
|
}
|
|
|
|
|
}, 480000); // 8 minutes
|
|
|
|
|
|
|
|
|
|
return () => clearTimeout(timeoutTimer);
|
|
|
|
|
}, [progress.status, progress.progress]);
|
|
|
|
|
|
|
|
|
|
const getCurrentStepInfo = () => {
|
|
|
|
|
// Try to match the current step from backend
|
|
|
|
|
const stepKey = progress.currentStep?.toLowerCase().replace(/\s+/g, '_');
|
|
|
|
|
return STEP_INFO_MAP[stepKey] || STEP_INFO_MAP['default'];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const formatTime = (seconds: number): string => {
|
|
|
|
|
if (!seconds || seconds <= 0) return '0m 0s';
|
|
|
|
|
const minutes = Math.floor(seconds / 60);
|
|
|
|
|
const remainingSeconds = seconds % 60;
|
|
|
|
|
return `${minutes}m ${remainingSeconds}s`;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getProgressSteps = () => {
|
|
|
|
|
// Create progress steps based on current progress percentage
|
|
|
|
|
const steps = [
|
|
|
|
|
{ id: 'data_validation', threshold: 25, name: 'Validación' },
|
|
|
|
|
{ id: 'feature_engineering', threshold: 50, name: 'Características' },
|
|
|
|
|
{ id: 'model_training', threshold: 80, name: 'Entrenamiento' },
|
|
|
|
|
{ id: 'model_validation', threshold: 100, name: 'Validación' }
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
return steps.map(step => ({
|
|
|
|
|
...step,
|
|
|
|
|
completed: progress.progress >= step.threshold,
|
|
|
|
|
current: progress.progress >= (step.threshold - 25) && progress.progress < step.threshold
|
|
|
|
|
}));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleContinueToDashboard = () => {
|
|
|
|
|
setShowTimeoutWarning(false);
|
|
|
|
|
if (onTimeout) {
|
|
|
|
|
onTimeout();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleKeepWaiting = () => {
|
|
|
|
|
setShowTimeoutWarning(false);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const currentStepInfo = getCurrentStepInfo();
|
|
|
|
|
const progressSteps = getProgressSteps();
|
|
|
|
|
|
|
|
|
|
// Handle error state
|
|
|
|
|
if (progress.status === 'failed' || progress.error) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="max-w-4xl mx-auto">
|
|
|
|
|
<div className="text-center mb-8">
|
|
|
|
|
<div className="inline-flex items-center justify-center w-20 h-20 bg-red-100 rounded-full mb-4">
|
|
|
|
|
<AlertCircle className="w-10 h-10 text-red-600" />
|
|
|
|
|
</div>
|
|
|
|
|
<h2 className="text-3xl font-bold text-gray-900 mb-2">
|
|
|
|
|
Error en el Entrenamiento
|
|
|
|
|
</h2>
|
|
|
|
|
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
|
|
|
|
|
Ha ocurrido un problema durante el entrenamiento. Nuestro equipo ha sido notificado.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-08-04 21:46:12 +02:00
|
|
|
<div className="bg-white rounded-md shadow-md p-8 mb-8">
|
|
|
|
|
<div className="bg-red-50 border border-red-200 rounded-md p-6">
|
2025-08-04 18:21:42 +02:00
|
|
|
<div className="flex items-start space-x-4">
|
|
|
|
|
<AlertCircle className="w-6 h-6 text-red-600 flex-shrink-0 mt-1" />
|
|
|
|
|
<div>
|
|
|
|
|
<h3 className="text-lg font-semibold text-red-800 mb-2">
|
|
|
|
|
Detalles del Error
|
|
|
|
|
</h3>
|
|
|
|
|
<p className="text-red-700">
|
|
|
|
|
{progress.error || 'Error desconocido durante el entrenamiento'}
|
|
|
|
|
</p>
|
|
|
|
|
<div className="mt-4 text-sm text-red-600">
|
|
|
|
|
<p>• Puedes intentar el entrenamiento nuevamente</p>
|
|
|
|
|
<p>• Verifica que tus datos históricos estén completos</p>
|
|
|
|
|
<p>• Contacta soporte si el problema persiste</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="mt-6 text-center">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => window.location.reload()}
|
|
|
|
|
className="bg-blue-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
Intentar Nuevamente
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="max-w-4xl mx-auto">
|
|
|
|
|
{/* Header */}
|
|
|
|
|
<div className="text-center mb-8">
|
|
|
|
|
<div className="inline-flex items-center justify-center w-20 h-20 bg-blue-600 rounded-full mb-4">
|
|
|
|
|
<Brain className="w-10 h-10 text-white animate-pulse" />
|
|
|
|
|
</div>
|
|
|
|
|
<h2 className="text-3xl font-bold text-gray-900 mb-2">
|
|
|
|
|
🧠 Entrenando tu modelo de predicción
|
|
|
|
|
</h2>
|
|
|
|
|
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
|
|
|
|
|
Estamos procesando tus datos históricos para crear predicciones personalizadas
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Main Progress Section */}
|
2025-08-04 21:46:12 +02:00
|
|
|
<div className="bg-white rounded-md shadow-md p-8 mb-8">
|
2025-08-04 18:21:42 +02:00
|
|
|
{/* Overall Progress Bar */}
|
|
|
|
|
<div className="mb-8">
|
|
|
|
|
<div className="flex justify-between items-center mb-3">
|
|
|
|
|
<span className="text-sm font-medium text-gray-700">Progreso General</span>
|
|
|
|
|
<span className="text-sm font-bold text-blue-600">{progress.progress}%</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="w-full bg-gray-200 rounded-full h-4 overflow-hidden">
|
|
|
|
|
<div
|
|
|
|
|
className="bg-gradient-to-r from-blue-500 to-indigo-600 h-4 rounded-full transition-all duration-1000 ease-out relative"
|
|
|
|
|
style={{ width: `${progress.progress}%` }}
|
|
|
|
|
>
|
|
|
|
|
<div className="absolute inset-0 opacity-20 animate-pulse">
|
|
|
|
|
<div className="h-full bg-white rounded-full"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Current Step Info */}
|
2025-08-04 21:46:12 +02:00
|
|
|
<div className={`bg-${currentStepInfo.color}-50 border border-${currentStepInfo.color}-200 rounded-md p-6 mb-6`}>
|
2025-08-04 18:21:42 +02:00
|
|
|
<div className="flex items-start space-x-4">
|
|
|
|
|
<div className="flex-shrink-0">
|
|
|
|
|
<div className={`w-12 h-12 bg-${currentStepInfo.color}-600 rounded-full flex items-center justify-center`}>
|
|
|
|
|
<currentStepInfo.icon className="w-6 h-6 text-white" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex-1">
|
|
|
|
|
<h3 className="text-xl font-semibold text-gray-900 mb-2">
|
|
|
|
|
{currentStepInfo.title}
|
|
|
|
|
</h3>
|
|
|
|
|
<p className="text-gray-700 mb-3">
|
|
|
|
|
{currentStepInfo.description}
|
|
|
|
|
</p>
|
|
|
|
|
<div className={`bg-${currentStepInfo.color}-100 border-l-4 border-${currentStepInfo.color}-500 p-3 rounded-r-lg`}>
|
|
|
|
|
<p className={`text-sm font-medium text-${currentStepInfo.color}-800`}>
|
|
|
|
|
{currentStepInfo.tip}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Step Progress Indicators */}
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
|
|
|
|
|
{progressSteps.map((step, index) => (
|
|
|
|
|
<div
|
|
|
|
|
key={step.id}
|
2025-08-04 21:46:12 +02:00
|
|
|
className={`p-4 rounded-md border-2 transition-all duration-300 ${
|
2025-08-04 18:21:42 +02:00
|
|
|
step.completed
|
|
|
|
|
? 'bg-green-50 border-green-200'
|
|
|
|
|
: step.current
|
|
|
|
|
? 'bg-blue-50 border-blue-300 shadow-md'
|
|
|
|
|
: 'bg-gray-50 border-gray-200'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center mb-2">
|
|
|
|
|
{step.completed ? (
|
|
|
|
|
<CheckCircle className="w-5 h-5 text-green-600 mr-2" />
|
|
|
|
|
) : step.current ? (
|
|
|
|
|
<div className="w-5 h-5 border-2 border-blue-600 border-t-transparent rounded-full animate-spin mr-2"></div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="w-5 h-5 border-2 border-gray-300 rounded-full mr-2"></div>
|
|
|
|
|
)}
|
|
|
|
|
<span className={`text-sm font-medium ${
|
|
|
|
|
step.completed ? 'text-green-800' : step.current ? 'text-blue-800' : 'text-gray-600'
|
|
|
|
|
}`}>
|
|
|
|
|
{step.name}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Enhanced Stats Grid */}
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
2025-08-04 21:46:12 +02:00
|
|
|
<div className="text-center p-4 bg-gray-50 rounded-md">
|
2025-08-04 18:21:42 +02:00
|
|
|
<div className="flex items-center justify-center mb-2">
|
|
|
|
|
<Cpu className="w-5 h-5 text-gray-600 mr-2" />
|
|
|
|
|
<span className="text-sm font-medium text-gray-700">Productos Procesados</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-2xl font-bold text-gray-900">
|
|
|
|
|
{progress.productsCompleted}/{progress.productsTotal || 'N/A'}
|
|
|
|
|
</div>
|
|
|
|
|
{progress.productsTotal > 0 && (
|
|
|
|
|
<div className="w-full bg-gray-200 rounded-full h-2 mt-2">
|
|
|
|
|
<div
|
|
|
|
|
className="bg-blue-500 h-2 rounded-full transition-all duration-500"
|
|
|
|
|
style={{ width: `${(progress.productsCompleted / progress.productsTotal) * 100}%` }}
|
|
|
|
|
></div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-08-04 21:46:12 +02:00
|
|
|
<div className="text-center p-4 bg-gray-50 rounded-md">
|
2025-08-04 18:21:42 +02:00
|
|
|
<div className="flex items-center justify-center mb-2">
|
|
|
|
|
<Clock className="w-5 h-5 text-gray-600 mr-2" />
|
|
|
|
|
<span className="text-sm font-medium text-gray-700">Tiempo Restante</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-2xl font-bold text-gray-900">
|
|
|
|
|
{progress.estimatedTimeRemaining
|
|
|
|
|
? formatTime(progress.estimatedTimeRemaining * 60) // Convert minutes to seconds
|
|
|
|
|
: 'Calculando...'
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-08-04 21:46:12 +02:00
|
|
|
<div className="text-center p-4 bg-gray-50 rounded-md">
|
2025-08-04 18:21:42 +02:00
|
|
|
<div className="flex items-center justify-center mb-2">
|
|
|
|
|
<Target className="w-5 h-5 text-gray-600 mr-2" />
|
|
|
|
|
<span className="text-sm font-medium text-gray-700">Precisión Esperada</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-2xl font-bold text-green-600">
|
|
|
|
|
~85%
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Status Indicator */}
|
|
|
|
|
<div className="mt-6 flex items-center justify-center">
|
|
|
|
|
<div className="flex items-center space-x-2 text-sm text-gray-600">
|
|
|
|
|
<Loader className="w-4 h-4 animate-spin" />
|
|
|
|
|
<span>Estado: {progress.status === 'running' ? 'Entrenando' : progress.status}</span>
|
|
|
|
|
<span>•</span>
|
|
|
|
|
<span>Paso actual: {progress.currentStep || 'Procesando...'}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Expected Benefits - Only show if progress < 80% to keep user engaged */}
|
|
|
|
|
{progress.progress < 80 && (
|
2025-08-04 21:46:12 +02:00
|
|
|
<div className="bg-white rounded-md shadow-md p-8">
|
2025-08-04 18:21:42 +02:00
|
|
|
<h3 className="text-2xl font-bold text-gray-900 mb-6 text-center">
|
|
|
|
|
Lo que podrás hacer una vez completado
|
|
|
|
|
</h3>
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
|
|
|
{EXPECTED_BENEFITS.map((benefit, index) => (
|
2025-08-04 21:46:12 +02:00
|
|
|
<div key={index} className="text-center p-6 bg-gradient-to-br from-indigo-50 to-purple-50 rounded-md">
|
2025-08-04 18:21:42 +02:00
|
|
|
<div className="inline-flex items-center justify-center w-12 h-12 bg-indigo-600 rounded-full mb-4">
|
|
|
|
|
<benefit.icon className="w-6 h-6 text-white" />
|
|
|
|
|
</div>
|
|
|
|
|
<h4 className="text-lg font-semibold text-gray-900 mb-2">
|
|
|
|
|
{benefit.title}
|
|
|
|
|
</h4>
|
|
|
|
|
<p className="text-gray-600">
|
|
|
|
|
{benefit.description}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Timeout Warning Modal */}
|
|
|
|
|
{showTimeoutWarning && (
|
|
|
|
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
2025-08-04 21:46:12 +02:00
|
|
|
<div className="bg-white rounded-md shadow-md p-8 max-w-md mx-4">
|
2025-08-04 18:21:42 +02:00
|
|
|
<div className="text-center">
|
|
|
|
|
<AlertCircle className="w-16 h-16 text-orange-500 mx-auto mb-4" />
|
|
|
|
|
<h3 className="text-xl font-bold text-gray-900 mb-4">
|
|
|
|
|
Entrenamiento tomando más tiempo
|
|
|
|
|
</h3>
|
|
|
|
|
<p className="text-gray-600 mb-6">
|
|
|
|
|
Puedes explorar el dashboard mientras terminamos el entrenamiento.
|
|
|
|
|
Te notificaremos cuando esté listo.
|
|
|
|
|
</p>
|
|
|
|
|
<div className="flex flex-col sm:flex-row gap-3">
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleContinueToDashboard}
|
|
|
|
|
className="flex-1 bg-blue-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
Continuar al Dashboard
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleKeepWaiting}
|
|
|
|
|
className="flex-1 bg-gray-200 text-gray-800 px-6 py-3 rounded-lg font-medium hover:bg-gray-300 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
Seguir Esperando
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|