Files
bakery-ia/frontend/src/components/EnhancedTrainingProgress.tsx
2025-08-04 18:58:12 +02:00

387 lines
15 KiB
TypeScript

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'
},
// Handle any unmapped steps
'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>
<div className="bg-white rounded-2xl shadow-xl p-8 mb-8">
<div className="bg-red-50 border border-red-200 rounded-xl p-6">
<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 */}
<div className="bg-white rounded-2xl shadow-xl p-8 mb-8">
{/* 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 */}
<div className={`bg-${currentStepInfo.color}-50 border border-${currentStepInfo.color}-200 rounded-xl p-6 mb-6`}>
<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}
className={`p-4 rounded-lg border-2 transition-all duration-300 ${
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">
<div className="text-center p-4 bg-gray-50 rounded-lg">
<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>
<div className="text-center p-4 bg-gray-50 rounded-lg">
<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>
<div className="text-center p-4 bg-gray-50 rounded-lg">
<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 && (
<div className="bg-white rounded-2xl shadow-xl p-8">
<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) => (
<div key={index} className="text-center p-6 bg-gradient-to-br from-indigo-50 to-purple-50 rounded-xl">
<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">
<div className="bg-white rounded-2xl shadow-2xl p-8 max-w-md mx-4">
<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>
);
}