diff --git a/frontend/src/components/EnhancedTrainingProgress.tsx b/frontend/src/components/EnhancedTrainingProgress.tsx new file mode 100644 index 00000000..7e6356d5 --- /dev/null +++ b/frontend/src/components/EnhancedTrainingProgress.tsx @@ -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 ( +
+
+
+ +
+

+ Error en el Entrenamiento +

+

+ Ha ocurrido un problema durante el entrenamiento. Nuestro equipo ha sido notificado. +

+
+ +
+
+
+ +
+

+ Detalles del Error +

+

+ {progress.error || 'Error desconocido durante el entrenamiento'} +

+
+

• Puedes intentar el entrenamiento nuevamente

+

• Verifica que tus datos históricos estén completos

+

• Contacta soporte si el problema persiste

+
+
+
+
+ +
+ +
+
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+ +
+

+ 🧠 Entrenando tu modelo de predicción +

+

+ Estamos procesando tus datos históricos para crear predicciones personalizadas +

+
+ + {/* Main Progress Section */} +
+ {/* Overall Progress Bar */} +
+
+ Progreso General + {progress.progress}% +
+
+
+
+
+
+
+
+
+ + {/* Current Step Info */} +
+
+
+
+ +
+
+
+

+ {currentStepInfo.title} +

+

+ {currentStepInfo.description} +

+
+

+ {currentStepInfo.tip} +

+
+
+
+
+ + {/* Step Progress Indicators */} +
+ {progressSteps.map((step, index) => ( +
+
+ {step.completed ? ( + + ) : step.current ? ( +
+ ) : ( +
+ )} + + {step.name} + +
+
+ ))} +
+ + {/* Enhanced Stats Grid */} +
+
+
+ + Productos Procesados +
+
+ {progress.productsCompleted}/{progress.productsTotal || 'N/A'} +
+ {progress.productsTotal > 0 && ( +
+
+
+ )} +
+ +
+
+ + Tiempo Restante +
+
+ {progress.estimatedTimeRemaining + ? formatTime(progress.estimatedTimeRemaining * 60) // Convert minutes to seconds + : 'Calculando...' + } +
+
+ +
+
+ + Precisión Esperada +
+
+ ~85% +
+
+
+ + {/* Status Indicator */} +
+
+ + Estado: {progress.status === 'running' ? 'Entrenando' : progress.status} + + Paso actual: {progress.currentStep || 'Procesando...'} +
+
+
+ + {/* Expected Benefits - Only show if progress < 80% to keep user engaged */} + {progress.progress < 80 && ( +
+

+ Lo que podrás hacer una vez completado +

+
+ {EXPECTED_BENEFITS.map((benefit, index) => ( +
+
+ +
+

+ {benefit.title} +

+

+ {benefit.description} +

+
+ ))} +
+
+ )} + + {/* Timeout Warning Modal */} + {showTimeoutWarning && ( +
+
+
+ +

+ Entrenamiento tomando más tiempo +

+

+ Puedes explorar el dashboard mientras terminamos el entrenamiento. + Te notificaremos cuando esté listo. +

+
+ + +
+
+
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/onboarding/OnboardingPage.tsx b/frontend/src/pages/onboarding/OnboardingPage.tsx index 0bff0c6e..90f799dc 100644 --- a/frontend/src/pages/onboarding/OnboardingPage.tsx +++ b/frontend/src/pages/onboarding/OnboardingPage.tsx @@ -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 toast from 'react-hot-toast'; +import EnhancedTrainingProgress from '../../components/EnhancedTrainingProgress'; + import { useTenant, useTraining, @@ -352,11 +354,30 @@ const OnboardingPage: React.FC = ({ user, onComplete }) => setCurrentStep(5); }; - const formatTimeRemaining = (seconds: number): string => { - const minutes = Math.floor(seconds / 60); - const secs = seconds % 60; - return `${minutes}:${secs.toString().padStart(2, '0')}`; - }; + const handleTrainingTimeout = () => { + // Option 1: Navigate to dashboard with limited functionality + onComplete(); // This calls your existing completion handler + + // 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: + const renderStep = () => { switch (currentStep) { @@ -672,133 +693,24 @@ const OnboardingPage: React.FC = ({ user, onComplete }) => ); case 4: - return ( -
-
-
- -
-

- 🧠 Entrenando tu modelo de predicción -

-

- Estamos procesando tus datos históricos para crear predicciones personalizadas -

-
- - {/* WebSocket Connection Status */} - {tenantId && trainingJobId && ( -
-
- {isConnected ? 'Conectado a actualizaciones en tiempo real' : 'Reconectando...'} -
- )} -
-
- {trainingProgress.currentStep} - - {trainingProgress.progress}% completado - -
- -
-
-
- - {trainingProgress.productsTotal > 0 && ( -
- - 📦 Productos: {trainingProgress.productsCompleted}/{trainingProgress.productsTotal} - - {trainingProgress.estimatedTimeRemaining > 0 && ( - - - {formatTimeRemaining(trainingProgress.estimatedTimeRemaining)} restante - - )} -
- )} -
- - {/* Training Status */} -
- {trainingProgress.status === 'running' && ( -
- -
-
Entrenamiento en progreso
-
- Tu modelo está aprendiendo de los patrones históricos de ventas -
-
-
- )} - - {trainingProgress.status === 'completed' && ( -
- -
-
¡Entrenamiento completado!
-
- Tu modelo está listo para generar predicciones precisas -
-
-
- )} - - {trainingProgress.status === 'failed' && ( -
-
- -
-
Error en el entrenamiento
-
- {trainingProgress.error || 'Ha ocurrido un error durante el entrenamiento'} -
-
-
- -
- - -
-
- )} -
- - {/* Educational Content */} -
-
-
¿Qué está pasando?
-
- Nuestro sistema está analizando patrones estacionales, tendencias de demanda y factores externos para crear un modelo personalizado para tu panadería. -
-
- -
-
Beneficios esperados
-
- Predicciones de demanda precisas, reducción de desperdicio, optimización de stock y mejor planificación de producción. -
-
-
-
- ); + return ( + { + // Handle timeout - either navigate to dashboard or show limited access + console.log('Training timeout - user wants to continue to dashboard'); + // You can add your custom timeout logic here + }} + /> + ); case 5: return ( diff --git a/services/training/app/api/training.py b/services/training/app/api/training.py index ed578a29..113d3a8d 100644 --- a/services/training/app/api/training.py +++ b/services/training/app/api/training.py @@ -80,28 +80,6 @@ async def start_training_job( requested_start=request.start_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 response_data = { @@ -174,11 +152,30 @@ async def execute_training_job_background( status_manager = TrainingStatusManager(db_session=db_session) - # Publish progress event - await publish_job_progress(job_id, tenant_id, 5, "Initializing training pipeline") - 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( job_id=job_id, status="running", diff --git a/services/training/app/ml/trainer.py b/services/training/app/ml/trainer.py index 530b6126..75f4bd79 100644 --- a/services/training/app/ml/trainer.py +++ b/services/training/app/ml/trainer.py @@ -10,6 +10,8 @@ import numpy as np from datetime import datetime import logging import uuid +import time +from datetime import datetime from app.ml.data_processor import BakeryDataProcessor from app.ml.prophet_manager import BakeryProphetManager @@ -75,6 +77,7 @@ class BakeryMLTrainer: processed_data = await self._process_all_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 logger.info("Training models for all products...") @@ -84,6 +87,7 @@ class BakeryMLTrainer: # Calculate overall training summary summary = self._calculate_training_summary(training_results) + await publish_job_progress(job_id, tenant_id, 90, "model_validation", estimated_time_remaining_minutes=1) result = { "job_id": job_id, @@ -354,6 +358,41 @@ class BakeryMLTrainer: 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, tenant_id: str, processed_data: Dict[str, pd.DataFrame], @@ -361,7 +400,17 @@ class BakeryMLTrainer: """Train models for all processed products using Prophet manager""" 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(): + product_start_time = time.time() try: 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)}' } 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 # 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, '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 diff --git a/services/training/app/services/training_orchestrator.py b/services/training/app/services/training_orchestrator.py index c2ea27a7..5babf99d 100644 --- a/services/training/app/services/training_orchestrator.py +++ b/services/training/app/services/training_orchestrator.py @@ -75,19 +75,13 @@ class TrainingDataOrchestrator: 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) # 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) 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 - 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( user_sales_range=sales_date_range, requested_start=requested_start, @@ -99,21 +93,15 @@ class TrainingDataOrchestrator: logger.info(f"Applied constraints: {aligned_range.constraints}") # 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) # Step 4: Collect external data sources concurrently 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( aligned_range, bakery_location, tenant_id ) # 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( filtered_sales, weather_data, traffic_data, aligned_range ) @@ -140,8 +128,6 @@ class TrainingDataOrchestrator: ) # 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) training_dataset.metadata["final_validation"] = final_validation diff --git a/services/training/app/services/training_service.py b/services/training/app/services/training_service.py index f6a85b6c..aed25136 100644 --- a/services/training/app/services/training_service.py +++ b/services/training/app/services/training_service.py @@ -78,7 +78,6 @@ class TrainingService: # Step 1: Prepare training dataset with date alignment and orchestration 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( tenant_id=tenant_id, bakery_location=bakery_location, @@ -86,10 +85,10 @@ class TrainingService: requested_end=requested_end, 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 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( tenant_id=tenant_id, training_dataset=training_dataset, @@ -117,7 +116,7 @@ class TrainingService: } 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) except Exception as e: