// Fixed Frontend Onboarding with Auto-Training // frontend/src/pages/onboarding.tsx import React, { useState, useEffect, useCallback } from 'react'; import { useRouter } from 'next/router'; import { CheckIcon, ArrowRightIcon, ArrowLeftIcon, CloudArrowUpIcon } from '@heroicons/react/24/outline'; import onboardingApi from '../api/onboardingApi'; const OnboardingPage = () => { const router = useRouter(); const [currentStep, setCurrentStep] = useState(1); const [completedSteps, setCompletedSteps] = useState([]); const [loading, setLoading] = useState(false); const [authToken, setAuthToken] = useState(null); const [tenantId, setTenantId] = useState(null); const [trainingTaskId, setTrainingTaskId] = useState(null); const [trainingStarted, setTrainingStarted] = useState(false); // New state to track training start const [formData, setFormData] = useState({ // Step 1: User Registration full_name: '', email: '', password: '', confirm_password: '', // Step 2: Bakery Information bakery_name: '', address: '', city: 'Madrid', postal_code: '', has_nearby_schools: false, has_nearby_offices: false, // Step 3: Sales History File salesFile: null as File | null, // Step 4: Model Training trainingStatus: 'pending' }); const [errors, setErrors] = useState<{ [key: string]: string }>({}); const [trainingProgress, setTrainingProgress] = useState({ currentTask: '', progress: 0, tasks: [ { id: 1, name: 'Procesando archivo de ventas históricas...', completed: false }, { id: 2, name: 'Preparando datos para el entrenamiento...', completed: false }, { id: 3, name: 'Entrenando modelos de pronóstico (esto puede tardar unos minutos)...', completed: false }, { id: 4, name: 'Evaluando y optimizando modelos...', completed: false }, { id: 5, name: 'Desplegando modelos en producción...', completed: false }, { id: 6, name: 'Entrenamiento completado.', completed: false }, ] }); // Load auth token and tenantId on component mount useEffect(() => { const token = localStorage.getItem('access_token'); const storedTenantId = localStorage.getItem('tenant_id'); if (token) { setAuthToken(token); onboardingApi.setAuthToken(token); } if (storedTenantId) { setTenantId(storedTenantId); } }, []); // Utility function to extract error message from FastAPI response const getErrorMessage = (error: any): string => { if (error.response && error.response.data && error.response.data.detail) { const detail = error.response.data.detail; if (typeof detail === 'string') { return detail; } if (Array.isArray(detail)) { return detail.map((err: any) => err.msg || JSON.stringify(err)).join(', '); } if (typeof detail === 'object') { return detail.msg || JSON.stringify(detail); } } return error.message || 'Ocurrió un error inesperado.'; }; const startModelTraining = useCallback(async () => { console.log('Starting model training...'); setLoading(true); setErrors({}); setTrainingStarted(true); // Mark training as started try { const response = await onboardingApi.startTraining(); console.log('Training API response:', response); setFormData(prev => ({ ...prev, trainingStatus: 'in_progress' })); setTrainingTaskId(response.data.task_id); setCompletedSteps(prev => [...prev, 4]); console.log('Training started successfully with task ID:', response.data.task_id); } catch (err: any) { console.error('Error starting training:', err); setErrors({ general: getErrorMessage(err) }); setTrainingStarted(false); // Reset if failed } finally { setLoading(false); } }, []); // Auto-start training when entering step 4 useEffect(() => { if (currentStep === 4 && !trainingStarted && !trainingTaskId && !loading) { console.log('Auto-starting training on step 4...'); startModelTraining(); } }, [currentStep, trainingStarted, trainingTaskId, loading, startModelTraining]); // Polling for training status useEffect(() => { let interval: NodeJS.Timeout; if (currentStep === 4 && trainingTaskId) { console.log(`Starting to poll for training status with task ID: ${trainingTaskId}`); interval = setInterval(async () => { try { const statusResponse = await onboardingApi.getTrainingStatus(trainingTaskId); console.log("Polling status:", statusResponse); const { status, progress, current_step, error: trainingError } = statusResponse.data; setTrainingProgress(prev => ({ ...prev, currentTask: current_step || 'Procesando...', progress: progress || 0, tasks: prev.tasks.map(task => task.name === current_step ? { ...task, completed: true } : task ), })); setFormData(prev => ({ ...prev, trainingStatus: status })); if (status === 'completed') { clearInterval(interval); setLoading(false); setCompletedSteps(prev => [...prev.filter(s => s !== 4), 4]); console.log('Training completed successfully!'); } else if (status === 'failed') { clearInterval(interval); setLoading(false); setTrainingStarted(false); // Allow retry setErrors({ general: trainingError || 'El entrenamiento falló.' }); console.error('Training failed:', trainingError); } } catch (error: any) { console.error('Error fetching training status:', error); clearInterval(interval); setLoading(false); setTrainingStarted(false); // Allow retry setErrors({ general: getErrorMessage(error) }); } }, 3000); // Poll every 3 seconds } return () => clearInterval(interval); }, [currentStep, trainingTaskId]); // Use useCallback for memoized functions const handleNext = useCallback(async () => { setLoading(true); setErrors({}); try { if (currentStep === 1) { if (formData.password !== formData.confirm_password) { setErrors({ confirmPassword: 'Las contraseñas no coinciden' }); setLoading(false); return; } const response = await onboardingApi.registerUser(formData); console.log('User registered:', response); if (response.data?.access_token) { localStorage.setItem('access_token', response.data.access_token); onboardingApi.setAuthToken(response.data.access_token); setAuthToken(response.data.access_token); } if (response.data?.tenant_id) { localStorage.setItem('tenant_id', response.data.tenant_id); setTenantId(response.data.tenant_id); } setCompletedSteps(prev => [...prev, 1]); setCurrentStep(2); } else if (currentStep === 2) { const response = await onboardingApi.registerBakery(formData); console.log('Bakery registered:', response); setCompletedSteps(prev => [...prev, 2]); setCurrentStep(3); } else if (currentStep === 3) { if (!formData.salesFile) { setErrors({ salesFile: 'Por favor, suba el archivo de historial de ventas.' }); setLoading(false); return; } const response = await onboardingApi.uploadSalesHistory(formData.salesFile); console.log('Sales history uploaded:', response); setCompletedSteps(prev => [...prev, 3]); setCurrentStep(4); // This will trigger auto-training via useEffect } } catch (err: any) { console.error('Error in step', currentStep, err); setErrors({ general: getErrorMessage(err) }); } finally { setLoading(false); } }, [currentStep, formData]); const handlePrevious = useCallback(() => { setCurrentStep(prev => Math.max(1, prev - 1)); }, []); const handleFileChange = useCallback((event: React.ChangeEvent) => { if (event.target.files && event.target.files[0]) { setFormData(prev => ({ ...prev, salesFile: event.target.files![0] })); setErrors(prev => ({ ...prev, salesFile: '' })); } }, []); const handleSubmitFinal = async () => { setLoading(true); setErrors({}); try { const response = await onboardingApi.completeOnboarding(); console.log('Onboarding completed:', response); router.push('/dashboard'); } catch (err: any) { console.error('Error completing onboarding:', err); setErrors({ general: getErrorMessage(err) }); } finally { setLoading(false); } }; const renderStep = () => { switch (currentStep) { case 1: return (

Paso 1: Datos de Usuario

Regístrate para comenzar a usar BakeryForecast.

{errors.general && (
{errors.general}
)}
setFormData(prev => ({ ...prev, full_name: e.target.value }))} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500" placeholder="Tu nombre completo" />
setFormData(prev => ({ ...prev, email: e.target.value }))} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500" placeholder="tu@email.com" />
setFormData(prev => ({ ...prev, password: e.target.value }))} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500" placeholder="••••••••" />
setFormData(prev => ({ ...prev, confirm_password: e.target.value }))} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500" placeholder="••••••••" /> {errors.confirmPassword &&

{errors.confirmPassword}

}
); case 2: return (

Paso 2: Información de la Panadería

Cuéntanos sobre tu panadería para personalizar las predicciones.

{errors.general && (
{errors.general}
)}
setFormData(prev => ({ ...prev, bakery_name: e.target.value }))} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500" placeholder="Panadería San José" />
setFormData(prev => ({ ...prev, address: e.target.value }))} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500" placeholder="Calle Mayor 123, Madrid" />
setFormData(prev => ({ ...prev, postal_code: e.target.value }))} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500" placeholder="28001" />
); case 3: return (

Paso 3: Historial de Ventas

Sube un archivo CSV con tu historial de ventas para entrenar el modelo de predicción.

{errors.general && (
{errors.general}
)} {errors.salesFile &&

{errors.salesFile}

}

o arrastra y suelta

Archivo CSV o Excel

{formData.salesFile && (

Archivo seleccionado: {formData.salesFile.name}

)}

Asegúrate de que tu archivo contiene columnas como: date, product_name, quantity_sold, revenue (opcional).

); case 4: return (

Paso 4: Entrenamiento del Modelo

{trainingStarted ? 'Estamos entrenando los modelos de predicción con tus datos de ventas. Esto puede tardar unos minutos.' : 'Preparándose para entrenar los modelos de predicción...' }

{errors.general && (
{errors.general}
)}

Estado del Entrenamiento

Progreso del entrenamiento del modelo de IA

Estado
{formData.trainingStatus === 'pending' ? 'En espera' : formData.trainingStatus === 'in_progress' ? 'En progreso' : formData.trainingStatus === 'completed' ? 'Completado' : formData.trainingStatus === 'failed' ? 'Fallido' : 'Desconocido'}
Progreso

{trainingProgress.progress}%

Paso Actual
{trainingProgress.currentTask || 'Iniciando...'}
Historial de Tareas
    {trainingProgress.tasks.map(task => (
  • {task.name} {task.completed && }
  • ))}
{/* Manual start button (only shown if auto-start failed) */} {!trainingStarted && formData.trainingStatus === 'pending' && !loading && (
)} {/* Retry button if training failed */} {formData.trainingStatus === 'failed' && !loading && (
)}
); case 5: return (

¡Enhorabuena!

Has completado el proceso de configuración. Tu sistema de predicción está listo para usar.

Sistema Configurado

Tu modelo de predicción ha sido entrenado y está listo para generar pronósticos precisos para tu panadería.

); default: return null; } }; const steps = [ { id: 1, name: 'Registro', status: currentStep > 1 || completedSteps.includes(1) ? 'complete' : currentStep === 1 ? 'current' : 'upcoming' }, { id: 2, name: 'Panadería', status: currentStep > 2 || completedSteps.includes(2) ? 'complete' : currentStep === 2 ? 'current' : 'upcoming' }, { id: 3, name: 'Historial de Ventas', status: currentStep > 3 || completedSteps.includes(3) ? 'complete' : currentStep === 3 ? 'current' : 'upcoming' }, { id: 4, name: 'Entrenamiento ML', status: currentStep > 4 || completedSteps.includes(4) ? 'complete' : currentStep === 4 ? 'current' : 'upcoming' }, { id: 5, name: 'Completar', status: currentStep > 5 || completedSteps.includes(5) ? 'complete' : currentStep === 5 ? 'current' : 'upcoming' }, ]; return (
{/* Progress Stepper */} {/* Step Content */}
{renderStep()}
{/* Navigation Buttons */}
{currentStep < 4 && ( )} {currentStep === 4 && ( )} {currentStep === 5 && ( )}
); }; export default OnboardingPage;