Fix new Frontend 12
This commit is contained in:
387
frontend/src/components/EnhancedTrainingProgress.tsx
Normal file
387
frontend/src/components/EnhancedTrainingProgress.tsx
Normal file
@@ -0,0 +1,387 @@
|
|||||||
|
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'
|
||||||
|
},
|
||||||
|
// Fallback for unknown 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ import React, { useState, useEffect, useCallback } from 'react';
|
|||||||
import { ChevronLeft, ChevronRight, Upload, MapPin, Store, Factory, Check, Brain, Clock, CheckCircle, AlertTriangle, Loader } from 'lucide-react';
|
import { ChevronLeft, ChevronRight, Upload, MapPin, Store, Factory, Check, Brain, Clock, CheckCircle, AlertTriangle, Loader } from 'lucide-react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
|
import EnhancedTrainingProgress from '../../components/EnhancedTrainingProgress';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useTenant,
|
useTenant,
|
||||||
useTraining,
|
useTraining,
|
||||||
@@ -352,11 +354,30 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
|
|||||||
setCurrentStep(5);
|
setCurrentStep(5);
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatTimeRemaining = (seconds: number): string => {
|
const handleTrainingTimeout = () => {
|
||||||
const minutes = Math.floor(seconds / 60);
|
// Option 1: Navigate to dashboard with limited functionality
|
||||||
const secs = seconds % 60;
|
onComplete(); // This calls your existing completion handler
|
||||||
return `${minutes}:${secs.toString().padStart(2, '0')}`;
|
|
||||||
};
|
// Option 2: Show a custom modal or message
|
||||||
|
// setShowLimitedAccessMessage(true);
|
||||||
|
|
||||||
|
// Option 3: Set a flag to enable partial dashboard access
|
||||||
|
// setLimitedAccess(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Then update the EnhancedTrainingProgress call:
|
||||||
|
<EnhancedTrainingProgress
|
||||||
|
progress={{
|
||||||
|
progress: trainingProgress.progress,
|
||||||
|
status: trainingProgress.status,
|
||||||
|
currentStep: trainingProgress.currentStep,
|
||||||
|
productsCompleted: trainingProgress.productsCompleted,
|
||||||
|
productsTotal: trainingProgress.productsTotal,
|
||||||
|
estimatedTimeRemaining: trainingProgress.estimatedTimeRemaining,
|
||||||
|
error: trainingProgress.error
|
||||||
|
}}
|
||||||
|
onTimeout={handleTrainingTimeout}
|
||||||
|
/>
|
||||||
|
|
||||||
const renderStep = () => {
|
const renderStep = () => {
|
||||||
switch (currentStep) {
|
switch (currentStep) {
|
||||||
@@ -672,133 +693,24 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
case 4:
|
case 4:
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<EnhancedTrainingProgress
|
||||||
<div className="text-center">
|
progress={{
|
||||||
<div className="mx-auto w-16 h-16 bg-primary-100 rounded-full flex items-center justify-center mb-4">
|
progress: trainingProgress.progress,
|
||||||
<Brain className="h-8 w-8 text-primary-600" />
|
status: trainingProgress.status,
|
||||||
</div>
|
currentStep: trainingProgress.currentStep,
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
productsCompleted: trainingProgress.productsCompleted,
|
||||||
🧠 Entrenando tu modelo de predicción
|
productsTotal: trainingProgress.productsTotal,
|
||||||
</h3>
|
estimatedTimeRemaining: trainingProgress.estimatedTimeRemaining,
|
||||||
<p className="text-gray-600 mb-8">
|
error: trainingProgress.error
|
||||||
Estamos procesando tus datos históricos para crear predicciones personalizadas
|
}}
|
||||||
</p>
|
onTimeout={() => {
|
||||||
</div>
|
// Handle timeout - either navigate to dashboard or show limited access
|
||||||
|
console.log('Training timeout - user wants to continue to dashboard');
|
||||||
{/* WebSocket Connection Status */}
|
// You can add your custom timeout logic here
|
||||||
{tenantId && trainingJobId && (
|
}}
|
||||||
<div className="mb-4 text-xs text-gray-500 flex items-center">
|
/>
|
||||||
<div className={`w-2 h-2 rounded-full mr-2 ${
|
);
|
||||||
isConnected ? 'bg-green-500' : 'bg-red-500'
|
|
||||||
}`} />
|
|
||||||
{isConnected ? 'Conectado a actualizaciones en tiempo real' : 'Reconectando...'}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex justify-between items-center text-sm">
|
|
||||||
<span className="text-gray-600">{trainingProgress.currentStep}</span>
|
|
||||||
<span className="text-gray-500">
|
|
||||||
{trainingProgress.progress}% completado
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-full bg-gray-200 rounded-full h-3">
|
|
||||||
<div
|
|
||||||
className="bg-gradient-to-r from-primary-500 to-primary-600 h-3 rounded-full transition-all duration-1000 ease-out"
|
|
||||||
style={{ width: `${trainingProgress.progress}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{trainingProgress.productsTotal > 0 && (
|
|
||||||
<div className="flex justify-between items-center text-sm text-gray-600">
|
|
||||||
<span>
|
|
||||||
📦 Productos: {trainingProgress.productsCompleted}/{trainingProgress.productsTotal}
|
|
||||||
</span>
|
|
||||||
{trainingProgress.estimatedTimeRemaining > 0 && (
|
|
||||||
<span className="flex items-center">
|
|
||||||
<Clock className="h-4 w-4 mr-1" />
|
|
||||||
{formatTimeRemaining(trainingProgress.estimatedTimeRemaining)} restante
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Training Status */}
|
|
||||||
<div className="bg-gray-50 rounded-xl p-6">
|
|
||||||
{trainingProgress.status === 'running' && (
|
|
||||||
<div className="flex items-center text-blue-700">
|
|
||||||
<Loader className="h-5 w-5 mr-3 animate-spin" />
|
|
||||||
<div>
|
|
||||||
<div className="font-medium">Entrenamiento en progreso</div>
|
|
||||||
<div className="text-sm text-blue-600">
|
|
||||||
Tu modelo está aprendiendo de los patrones históricos de ventas
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{trainingProgress.status === 'completed' && (
|
|
||||||
<div className="flex items-center text-green-700">
|
|
||||||
<CheckCircle className="h-5 w-5 mr-3" />
|
|
||||||
<div>
|
|
||||||
<div className="font-medium">¡Entrenamiento completado!</div>
|
|
||||||
<div className="text-sm text-green-600">
|
|
||||||
Tu modelo está listo para generar predicciones precisas
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{trainingProgress.status === 'failed' && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex items-center text-red-700">
|
|
||||||
<AlertTriangle className="h-5 w-5 mr-3" />
|
|
||||||
<div>
|
|
||||||
<div className="font-medium">Error en el entrenamiento</div>
|
|
||||||
<div className="text-sm text-red-600">
|
|
||||||
{trainingProgress.error || 'Ha ocurrido un error durante el entrenamiento'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex space-x-3">
|
|
||||||
<button
|
|
||||||
onClick={handleRetryTraining}
|
|
||||||
className="flex-1 px-4 py-2 bg-primary-500 text-white rounded-lg hover:bg-primary-600 transition-colors"
|
|
||||||
>
|
|
||||||
Reintentar entrenamiento
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleSkipTraining}
|
|
||||||
className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
|
||||||
>
|
|
||||||
Continuar sin entrenar
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Educational Content */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
|
||||||
<div className="bg-blue-50 p-4 rounded-lg">
|
|
||||||
<div className="font-medium text-blue-900 mb-2">¿Qué está pasando?</div>
|
|
||||||
<div className="text-blue-700">
|
|
||||||
Nuestro sistema está analizando patrones estacionales, tendencias de demanda y factores externos para crear un modelo personalizado para tu panadería.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-green-50 p-4 rounded-lg">
|
|
||||||
<div className="font-medium text-green-900 mb-2">Beneficios esperados</div>
|
|
||||||
<div className="text-green-700">
|
|
||||||
Predicciones de demanda precisas, reducción de desperdicio, optimización de stock y mejor planificación de producción.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 5:
|
case 5:
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -80,28 +80,6 @@ async def start_training_job(
|
|||||||
requested_start=request.start_date,
|
requested_start=request.start_date,
|
||||||
requested_end=request.end_date
|
requested_end=request.end_date
|
||||||
)
|
)
|
||||||
|
|
||||||
training_config = {
|
|
||||||
"job_id": job_id,
|
|
||||||
"tenant_id": tenant_id,
|
|
||||||
"bakery_location": {
|
|
||||||
"latitude": 40.4168,
|
|
||||||
"longitude": -3.7038
|
|
||||||
},
|
|
||||||
"requested_start": request.start_date.isoformat() if request.start_date else None,
|
|
||||||
"requested_end": request.end_date.isoformat() if request.end_date else None,
|
|
||||||
"estimated_duration_minutes": 15,
|
|
||||||
"estimated_products": 10,
|
|
||||||
"background_execution": True,
|
|
||||||
"api_version": "v1"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Publish immediate event (training started)
|
|
||||||
await publish_job_started(
|
|
||||||
job_id=job_id,
|
|
||||||
tenant_id=tenant_id,
|
|
||||||
config=training_config
|
|
||||||
)
|
|
||||||
|
|
||||||
# Return immediate success response
|
# Return immediate success response
|
||||||
response_data = {
|
response_data = {
|
||||||
@@ -174,11 +152,30 @@ async def execute_training_job_background(
|
|||||||
|
|
||||||
status_manager = TrainingStatusManager(db_session=db_session)
|
status_manager = TrainingStatusManager(db_session=db_session)
|
||||||
|
|
||||||
# Publish progress event
|
|
||||||
await publish_job_progress(job_id, tenant_id, 5, "Initializing training pipeline")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
||||||
|
training_config = {
|
||||||
|
"job_id": job_id,
|
||||||
|
"tenant_id": tenant_id,
|
||||||
|
"bakery_location": {
|
||||||
|
"latitude": 40.4168,
|
||||||
|
"longitude": -3.7038
|
||||||
|
},
|
||||||
|
"requested_start": requested_start if requested_start else None,
|
||||||
|
"requested_end": requested_end if requested_end else None,
|
||||||
|
"estimated_duration_minutes": 15,
|
||||||
|
"estimated_products": None,
|
||||||
|
"background_execution": True,
|
||||||
|
"api_version": "v1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Publish immediate event (training started)
|
||||||
|
await publish_job_started(
|
||||||
|
job_id=job_id,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
config=training_config
|
||||||
|
)
|
||||||
|
|
||||||
await status_manager.update_job_status(
|
await status_manager.update_job_status(
|
||||||
job_id=job_id,
|
job_id=job_id,
|
||||||
status="running",
|
status="running",
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import numpy as np
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from app.ml.data_processor import BakeryDataProcessor
|
from app.ml.data_processor import BakeryDataProcessor
|
||||||
from app.ml.prophet_manager import BakeryProphetManager
|
from app.ml.prophet_manager import BakeryProphetManager
|
||||||
@@ -75,6 +77,7 @@ class BakeryMLTrainer:
|
|||||||
processed_data = await self._process_all_products(
|
processed_data = await self._process_all_products(
|
||||||
sales_df, weather_df, traffic_df, products
|
sales_df, weather_df, traffic_df, products
|
||||||
)
|
)
|
||||||
|
await publish_job_progress(job_id, tenant_id, 20, "feature_engineering", estimated_time_remaining_minutes=7)
|
||||||
|
|
||||||
# Train models for each processed product
|
# Train models for each processed product
|
||||||
logger.info("Training models for all products...")
|
logger.info("Training models for all products...")
|
||||||
@@ -84,6 +87,7 @@ class BakeryMLTrainer:
|
|||||||
|
|
||||||
# Calculate overall training summary
|
# Calculate overall training summary
|
||||||
summary = self._calculate_training_summary(training_results)
|
summary = self._calculate_training_summary(training_results)
|
||||||
|
await publish_job_progress(job_id, tenant_id, 90, "model_validation", estimated_time_remaining_minutes=1)
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
"job_id": job_id,
|
"job_id": job_id,
|
||||||
@@ -354,6 +358,41 @@ class BakeryMLTrainer:
|
|||||||
|
|
||||||
return processed_data
|
return processed_data
|
||||||
|
|
||||||
|
def calculate_estimated_time_remaining(self, processing_times: List[float], completed: int, total: int) -> int:
|
||||||
|
"""
|
||||||
|
Calculate estimated time remaining based on actual processing times
|
||||||
|
|
||||||
|
Args:
|
||||||
|
processing_times: List of processing times for completed items (in seconds)
|
||||||
|
completed: Number of items completed so far
|
||||||
|
total: Total number of items to process
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Estimated time remaining in minutes
|
||||||
|
"""
|
||||||
|
if not processing_times or completed >= total:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Calculate average processing time
|
||||||
|
avg_time_per_item = sum(processing_times) / len(processing_times)
|
||||||
|
|
||||||
|
# Use weighted average giving more weight to recent processing times
|
||||||
|
if len(processing_times) > 3:
|
||||||
|
# Use last 3 items for more accurate recent performance
|
||||||
|
recent_times = processing_times[-3:]
|
||||||
|
recent_avg = sum(recent_times) / len(recent_times)
|
||||||
|
# Weighted average: 70% recent, 30% overall
|
||||||
|
avg_time_per_item = (recent_avg * 0.7) + (avg_time_per_item * 0.3)
|
||||||
|
|
||||||
|
# Calculate remaining items and estimated time
|
||||||
|
remaining_items = total - completed
|
||||||
|
estimated_seconds = remaining_items * avg_time_per_item
|
||||||
|
|
||||||
|
# Convert to minutes and round up
|
||||||
|
estimated_minutes = max(1, int(estimated_seconds / 60) + (1 if estimated_seconds % 60 > 0 else 0))
|
||||||
|
|
||||||
|
return estimated_minutes
|
||||||
|
|
||||||
async def _train_all_models(self,
|
async def _train_all_models(self,
|
||||||
tenant_id: str,
|
tenant_id: str,
|
||||||
processed_data: Dict[str, pd.DataFrame],
|
processed_data: Dict[str, pd.DataFrame],
|
||||||
@@ -361,7 +400,17 @@ class BakeryMLTrainer:
|
|||||||
"""Train models for all processed products using Prophet manager"""
|
"""Train models for all processed products using Prophet manager"""
|
||||||
training_results = {}
|
training_results = {}
|
||||||
|
|
||||||
|
total_products = len(processed_data)
|
||||||
|
base_progress = 45
|
||||||
|
max_progress = 85 # or whatever your target end progress is
|
||||||
|
products_total = 0
|
||||||
|
i = 0
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
processing_times = [] # Store individual processing times
|
||||||
|
|
||||||
for product_name, product_data in processed_data.items():
|
for product_name, product_data in processed_data.items():
|
||||||
|
product_start_time = time.time()
|
||||||
try:
|
try:
|
||||||
logger.info(f"Training model for product: {product_name}")
|
logger.info(f"Training model for product: {product_name}")
|
||||||
|
|
||||||
@@ -375,6 +424,7 @@ class BakeryMLTrainer:
|
|||||||
'message': f'Need at least {settings.MIN_TRAINING_DATA_DAYS} data points, got {len(product_data)}'
|
'message': f'Need at least {settings.MIN_TRAINING_DATA_DAYS} data points, got {len(product_data)}'
|
||||||
}
|
}
|
||||||
logger.warning(f"Skipping {product_name}: insufficient data ({len(product_data)} < {settings.MIN_TRAINING_DATA_DAYS})")
|
logger.warning(f"Skipping {product_name}: insufficient data ({len(product_data)} < {settings.MIN_TRAINING_DATA_DAYS})")
|
||||||
|
processing_times.append(time.time() - product_start_time)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Train the model using Prophet manager
|
# Train the model using Prophet manager
|
||||||
@@ -402,6 +452,29 @@ class BakeryMLTrainer:
|
|||||||
'data_points': len(product_data) if product_data is not None else 0,
|
'data_points': len(product_data) if product_data is not None else 0,
|
||||||
'failed_at': datetime.now().isoformat()
|
'failed_at': datetime.now().isoformat()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Record processing time for this product
|
||||||
|
product_processing_time = time.time() - product_start_time
|
||||||
|
processing_times.append(product_processing_time)
|
||||||
|
|
||||||
|
i += 1
|
||||||
|
current_progress = base_progress + int((i / total_products) * (max_progress - base_progress))
|
||||||
|
|
||||||
|
# Calculate estimated time remaining
|
||||||
|
estimated_time_remaining_minutes = self.calculate_estimated_time_remaining(
|
||||||
|
processing_times, i, total_products
|
||||||
|
)
|
||||||
|
|
||||||
|
await publish_job_progress(
|
||||||
|
job_id,
|
||||||
|
tenant_id,
|
||||||
|
current_progress,
|
||||||
|
"model_training",
|
||||||
|
product_name,
|
||||||
|
products_total,
|
||||||
|
total_products,
|
||||||
|
estimated_time_remaining_minutes=estimated_time_remaining_minutes
|
||||||
|
)
|
||||||
|
|
||||||
return training_results
|
return training_results
|
||||||
|
|
||||||
|
|||||||
@@ -75,19 +75,13 @@ class TrainingDataOrchestrator:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
||||||
await publish_job_progress(job_id, tenant_id, 5, "Extrayendo datos de ventas",
|
|
||||||
step_details="Conectando con servicio de datos")
|
|
||||||
sales_data = await self.data_client.fetch_sales_data(tenant_id)
|
sales_data = await self.data_client.fetch_sales_data(tenant_id)
|
||||||
|
|
||||||
# Step 1: Extract and validate sales data date range
|
# Step 1: Extract and validate sales data date range
|
||||||
await publish_job_progress(job_id, tenant_id, 10, "Validando fechas de datos de venta",
|
|
||||||
step_details="Aplicando restricciones de fuentes de datos")
|
|
||||||
sales_date_range = self._extract_sales_date_range(sales_data)
|
sales_date_range = self._extract_sales_date_range(sales_data)
|
||||||
logger.info(f"Sales data range detected: {sales_date_range.start} to {sales_date_range.end}")
|
logger.info(f"Sales data range detected: {sales_date_range.start} to {sales_date_range.end}")
|
||||||
|
|
||||||
# Step 2: Apply date alignment across all data sources
|
# Step 2: Apply date alignment across all data sources
|
||||||
await publish_job_progress(job_id, tenant_id, 15, "Alinear el rango de fechas",
|
|
||||||
step_details="Aplicar la alineación de fechas en todas las fuentes de datos")
|
|
||||||
aligned_range = self.date_alignment_service.validate_and_align_dates(
|
aligned_range = self.date_alignment_service.validate_and_align_dates(
|
||||||
user_sales_range=sales_date_range,
|
user_sales_range=sales_date_range,
|
||||||
requested_start=requested_start,
|
requested_start=requested_start,
|
||||||
@@ -99,21 +93,15 @@ class TrainingDataOrchestrator:
|
|||||||
logger.info(f"Applied constraints: {aligned_range.constraints}")
|
logger.info(f"Applied constraints: {aligned_range.constraints}")
|
||||||
|
|
||||||
# Step 3: Filter sales data to aligned date range
|
# Step 3: Filter sales data to aligned date range
|
||||||
await publish_job_progress(job_id, tenant_id, 20, "Alinear el rango de las ventas",
|
|
||||||
step_details="Aplicar la alineación de fechas de las ventas")
|
|
||||||
filtered_sales = self._filter_sales_data(sales_data, aligned_range)
|
filtered_sales = self._filter_sales_data(sales_data, aligned_range)
|
||||||
|
|
||||||
# Step 4: Collect external data sources concurrently
|
# Step 4: Collect external data sources concurrently
|
||||||
logger.info("Collecting external data sources...")
|
logger.info("Collecting external data sources...")
|
||||||
await publish_job_progress(job_id, tenant_id, 25, "Recopilación de fuentes de datos externas",
|
|
||||||
step_details="Recopilación de fuentes de datos externas")
|
|
||||||
weather_data, traffic_data = await self._collect_external_data(
|
weather_data, traffic_data = await self._collect_external_data(
|
||||||
aligned_range, bakery_location, tenant_id
|
aligned_range, bakery_location, tenant_id
|
||||||
)
|
)
|
||||||
|
|
||||||
# Step 5: Validate data quality
|
# Step 5: Validate data quality
|
||||||
await publish_job_progress(job_id, tenant_id, 30, "Validando la calidad de los datos",
|
|
||||||
step_details="Validando la calidad de los datos")
|
|
||||||
data_quality_results = self._validate_data_sources(
|
data_quality_results = self._validate_data_sources(
|
||||||
filtered_sales, weather_data, traffic_data, aligned_range
|
filtered_sales, weather_data, traffic_data, aligned_range
|
||||||
)
|
)
|
||||||
@@ -140,8 +128,6 @@ class TrainingDataOrchestrator:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Step 7: Final validation
|
# Step 7: Final validation
|
||||||
await publish_job_progress(job_id, tenant_id, 35, "Validancion final de los datos",
|
|
||||||
step_details="Validancion final de los datos")
|
|
||||||
final_validation = self.validate_training_data_quality(training_dataset)
|
final_validation = self.validate_training_data_quality(training_dataset)
|
||||||
training_dataset.metadata["final_validation"] = final_validation
|
training_dataset.metadata["final_validation"] = final_validation
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,6 @@ class TrainingService:
|
|||||||
|
|
||||||
# Step 1: Prepare training dataset with date alignment and orchestration
|
# Step 1: Prepare training dataset with date alignment and orchestration
|
||||||
logger.info("Step 1: Preparing and aligning training data")
|
logger.info("Step 1: Preparing and aligning training data")
|
||||||
await publish_job_progress(job_id, tenant_id, 0, "Extrayendo datos de ventas")
|
|
||||||
training_dataset = await self.orchestrator.prepare_training_data(
|
training_dataset = await self.orchestrator.prepare_training_data(
|
||||||
tenant_id=tenant_id,
|
tenant_id=tenant_id,
|
||||||
bakery_location=bakery_location,
|
bakery_location=bakery_location,
|
||||||
@@ -86,10 +85,10 @@ class TrainingService:
|
|||||||
requested_end=requested_end,
|
requested_end=requested_end,
|
||||||
job_id=job_id
|
job_id=job_id
|
||||||
)
|
)
|
||||||
|
await publish_job_progress(job_id, tenant_id, 10, "data_validation", estimated_time_remaining_minutes=8)
|
||||||
|
|
||||||
# Step 2: Execute ML training pipeline
|
# Step 2: Execute ML training pipeline
|
||||||
logger.info("Step 2: Starting ML training pipeline")
|
logger.info("Step 2: Starting ML training pipeline")
|
||||||
await publish_job_progress(job_id, tenant_id, 35, "Starting ML training pipeline")
|
|
||||||
training_results = await self.trainer.train_tenant_models(
|
training_results = await self.trainer.train_tenant_models(
|
||||||
tenant_id=tenant_id,
|
tenant_id=tenant_id,
|
||||||
training_dataset=training_dataset,
|
training_dataset=training_dataset,
|
||||||
@@ -117,7 +116,7 @@ class TrainingService:
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.info(f"Training job {job_id} completed successfully")
|
logger.info(f"Training job {job_id} completed successfully")
|
||||||
await publish_job_completed(job_id, tenant_id, final_result);
|
await publish_job_completed(job_id, tenant_id, final_result)
|
||||||
return TrainingService.create_detailed_training_response(final_result)
|
return TrainingService.create_detailed_training_response(final_result)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
Reference in New Issue
Block a user