Files
bakery-ia/frontend/src/pages/onboarding/OnboardingPage.tsx
2025-08-04 21:46:12 +02:00

924 lines
36 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
useData,
useTrainingWebSocket,
TenantCreate,
TrainingJobRequest
} from '../../api';
interface OnboardingPageProps {
user: any;
onComplete: () => void;
}
interface BakeryData {
name: string;
address: string;
businessType: 'individual' | 'central_workshop';
coordinates?: { lat: number; lng: number };
products: string[];
hasHistoricalData: boolean;
csvFile?: File;
}
interface TrainingProgress {
progress: number;
status: string;
currentStep: string;
productsCompleted: number;
productsTotal: number;
estimatedTimeRemaining: number;
error?: string;
}
const MADRID_PRODUCTS = [
'Croissants', 'Pan de molde', 'Baguettes', 'Panecillos', 'Ensaimadas',
'Napolitanas', 'Magdalenas', 'Donuts', 'Palmeras', 'Café',
'Chocolate caliente', 'Zumos', 'Bocadillos', 'Empanadas', 'Tartas'
];
const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) => {
const [currentStep, setCurrentStep] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [bakeryData, setBakeryData] = useState<BakeryData>({
name: '',
address: '',
businessType: 'individual',
products: MADRID_PRODUCTS, // Automatically assign all products
hasHistoricalData: false
});
// Training progress state
const [tenantId, setTenantId] = useState<string>('');
const [trainingJobId, setTrainingJobId] = useState<string>('');
const { createTenant, isLoading: tenantLoading } = useTenant();
const { startTrainingJob } = useTraining({ disablePolling: true });
const { uploadSalesHistory, validateSalesData } = useData();
const steps = [
{ id: 1, title: 'Datos de Panadería', icon: Store },
{ id: 2, title: 'Datos Históricos', icon: Upload },
{ id: 3, title: 'Entrenamiento IA', icon: Brain },
{ id: 4, title: 'Configuración Final', icon: Check }
];
const [trainingProgress, setTrainingProgress] = useState<TrainingProgress>({
progress: 0,
status: 'pending',
currentStep: 'Iniciando...',
productsCompleted: 0,
productsTotal: 0,
estimatedTimeRemaining: 0
});
// WebSocket connection for real-time training updates
const {
status,
jobUpdates,
connect,
disconnect,
isConnected,
lastMessage,
tenantId: resolvedTenantId,
wsUrl
} = useTrainingWebSocket(trainingJobId || 'pending', tenantId);
// Handle WebSocket job updates
const processWebSocketMessage = useCallback((message: any) => {
const messageType = message.type;
const data = message.data || message; // Fallback if data is at root level
if (messageType === 'progress' || messageType === 'training_progress') {
setTrainingProgress(prev => ({
...prev,
progress: typeof data.progress === 'number' ? data.progress : prev.progress,
currentStep: data.current_step || data.currentStep || 'Procesando...',
productsCompleted: data.products_completed || data.productsCompleted || prev.productsCompleted,
productsTotal: data.products_total || data.productsTotal || prev.productsTotal,
estimatedTimeRemaining: data.estimated_time_remaining_minutes ||
data.estimated_time_remaining ||
data.estimatedTimeRemaining ||
prev.estimatedTimeRemaining,
status: 'running'
}));
} else if (messageType === 'completed' || messageType === 'training_completed') {
setTrainingProgress(prev => ({
...prev,
progress: 100,
status: 'completed',
currentStep: 'Entrenamiento completado',
estimatedTimeRemaining: 0
}));
// Auto-advance to final step after 2 seconds
setTimeout(() => {
setCurrentStep(4);
}, 2000);
} else if (messageType === 'failed' || messageType === 'training_failed' || messageType === 'training_error') {
setTrainingProgress(prev => ({
...prev,
status: 'failed',
error: data.error || data.message || 'Error en el entrenamiento',
currentStep: 'Error en el entrenamiento'
}));
} else if (messageType === 'initial_status') {
setTrainingProgress(prev => ({
...prev,
progress: typeof data.progress === 'number' ? data.progress : prev.progress,
status: data.status || prev.status,
currentStep: data.current_step || data.currentStep || prev.currentStep
}));
}
}, []);
// Process WebSocket messages
useEffect(() => {
if (lastMessage) {
processWebSocketMessage(lastMessage);
}
}, [lastMessage, processWebSocketMessage]);
// Backup jobUpdates processing
useEffect(() => {
if (jobUpdates.length > 0) {
const latestUpdate = jobUpdates[0];
processWebSocketMessage(latestUpdate);
}
}, [jobUpdates, processWebSocketMessage]);
// Connect to WebSocket when training starts
useEffect(() => {
if (tenantId && trainingJobId && currentStep === 3) {
connect();
}
return () => {
if (isConnected) {
disconnect();
}
};
}, [tenantId, trainingJobId, currentStep, connect, disconnect, isConnected]);
const storeTenantId = (tenantId: string) => {
try {
// Method 1: Store tenant ID directly
localStorage.setItem('current_tenant_id', tenantId);
// Method 2: Update user_data to include tenant_id
const existingUserData = localStorage.getItem('user_data');
if (existingUserData) {
const userData = JSON.parse(existingUserData);
userData.current_tenant_id = tenantId;
userData.tenant_id = tenantId; // Backup key
localStorage.setItem('user_data', JSON.stringify(userData));
} else {
// Create user_data with tenant info if it doesn't exist
localStorage.setItem('user_data', JSON.stringify({
current_tenant_id: tenantId,
tenant_id: tenantId
}));
}
// Method 3: Store in a dedicated tenant context
localStorage.setItem('tenant_context', JSON.stringify({
current_tenant_id: tenantId,
last_updated: new Date().toISOString()
}));
console.log('✅ Tenant ID stored successfully:', tenantId);
} catch (error) {
console.error('❌ Failed to store tenant ID:', error);
}
};
const handleNext = () => {
if (validateCurrentStep()) {
if (currentStep === 2) {
// Always proceed to training step after CSV upload
startTraining();
} else {
setCurrentStep(prev => Math.min(prev + 1, steps.length));
}
}
};
const handlePrevious = () => {
setCurrentStep(prev => Math.max(prev - 1, 1));
};
const validateCurrentStep = (): boolean => {
switch (currentStep) {
case 1:
if (!bakeryData.name.trim()) {
toast.error('El nombre de la panadería es obligatorio');
return false;
}
if (!bakeryData.address.trim()) {
toast.error('La dirección es obligatoria');
return false;
}
return true;
case 2:
if (!bakeryData.csvFile) {
toast.error('Por favor, selecciona un archivo con tus datos históricos');
return false;
}
// Validate file format
const fileName = bakeryData.csvFile.name.toLowerCase();
const supportedFormats = ['.csv', '.xlsx', '.xls', '.json'];
const isValidFormat = supportedFormats.some(format => fileName.endsWith(format));
if (!isValidFormat) {
toast.error('Formato de archivo no soportado. Usa CSV, Excel (.xlsx, .xls) o JSON');
return false;
}
// Validate file size (10MB limit as per backend)
const maxSize = 10 * 1024 * 1024;
if (bakeryData.csvFile.size > maxSize) {
toast.error('El archivo es demasiado grande. Máximo 10MB');
return false;
}
return true;
default:
return true;
}
};
const startTraining = async () => {
setCurrentStep(3);
setIsLoading(true);
try {
const token = localStorage.getItem('auth_token');
if (!token) {
toast.error('Sesión expirada. Por favor, inicia sesión nuevamente.');
return;
}
// Create tenant first
const tenantData: TenantCreate = {
name: bakeryData.name,
address: bakeryData.address,
business_type: "bakery",
postal_code: "28010",
phone: "+34655334455",
coordinates: bakeryData.coordinates,
products: bakeryData.products,
has_historical_data: bakeryData.hasHistoricalData,
};
const tenant = await createTenant(tenantData);
setTenantId(tenant.id);
storeTenantId(tenant.id);
// Step 2: Validate and Upload CSV file if provided
if (bakeryData.csvFile) {
try {
const validationResult = await validateSalesData(tenant.id, bakeryData.csvFile);
if (!validationResult.is_valid) {
toast.error(`Error en los datos: ${validationResult.message}`);
setTrainingProgress(prev => ({
...prev,
status: 'failed',
error: 'Error en la validación de datos históricos'
}));
return;
}
await uploadSalesHistory(tenant.id, bakeryData.csvFile);
toast.success('Datos históricos validados y subidos correctamente');
} catch (error) {
console.error('CSV validation/upload error:', error);
toast.error('Error al procesar los datos históricos');
setTrainingProgress(prev => ({
...prev,
status: 'failed',
error: 'Error al procesar los datos históricos'
}));
return;
}
}
// Prepare training job request - always use uploaded data since CSV is required
const trainingRequest: TrainingJobRequest = {
include_weather: true,
include_traffic: false,
min_data_points: 30,
use_default_data: false // Always false since CSV upload is mandatory
};
// Start training job using the proper API
const trainingJob = await startTrainingJob(tenant.id, trainingRequest);
setTrainingJobId(trainingJob.job_id);
setTrainingProgress({
progress: 0,
status: 'running',
currentStep: 'Iniciando entrenamiento...',
productsCompleted: 0,
productsTotal: bakeryData.products.length,
estimatedTimeRemaining: 600 // 10 minutes
});
toast.success('Entrenamiento iniciado correctamente');
} catch (error) {
console.error('Training start error:', error);
toast.error('Error al iniciar el entrenamiento');
setTrainingProgress(prev => ({
...prev,
status: 'failed',
error: 'Error al iniciar el entrenamiento'
}));
} finally {
setIsLoading(false);
}
};
const handleComplete = async () => {
if (!validateCurrentStep()) return;
if (currentStep < 3) {
// Start training process
await startTraining();
} else {
// Complete onboarding
toast.success('¡Configuración completada exitosamente!');
onComplete();
}
};
const handleRetryTraining = async () => {
setTrainingProgress({
progress: 0,
status: 'pending',
currentStep: 'Preparando reintento...',
productsCompleted: 0,
productsTotal: bakeryData.products.length,
estimatedTimeRemaining: 600
});
await startTraining();
};
const handleSkipTraining = () => {
toast('Continuando sin entrenamiento. Podrás entrenar los modelos más tarde desde el dashboard.', {
icon: '',
duration: 4000
});
setCurrentStep(4);
};
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:
<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 = () => {
switch (currentStep) {
case 1:
return (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-4">
Información de tu Panadería
</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Nombre de la panadería
</label>
<input
type="text"
value={bakeryData.name}
onChange={(e) => setBakeryData(prev => ({ ...prev, name: e.target.value }))}
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
placeholder="Ej: Panadería San Miguel"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Dirección completa
</label>
<div className="relative">
<MapPin className="absolute left-3 top-3 h-5 w-5 text-gray-400" />
<input
type="text"
value={bakeryData.address}
onChange={(e) => setBakeryData(prev => ({ ...prev, address: e.target.value }))}
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
placeholder="Calle Mayor, 123, Madrid"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-3">
Tipo de negocio
</label>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<button
type="button"
onClick={() => setBakeryData(prev => ({ ...prev, businessType: 'individual' }))}
className={`p-4 border rounded-xl text-left transition-all ${
bakeryData.businessType === 'individual'
? 'border-primary-500 bg-primary-50 text-primary-700'
: 'border-gray-300 hover:border-gray-400'
}`}
>
<Store className="h-5 w-5 mb-2" />
<div className="font-medium">Panadería Individual</div>
<div className="text-sm text-gray-500">Un solo punto de venta</div>
</button>
<button
type="button"
onClick={() => setBakeryData(prev => ({ ...prev, businessType: 'central_workshop' }))}
className={`p-4 border rounded-xl text-left transition-all ${
bakeryData.businessType === 'central_workshop'
? 'border-primary-500 bg-primary-50 text-primary-700'
: 'border-gray-300 hover:border-gray-400'
}`}
>
<Factory className="h-5 w-5 mb-2" />
<div className="font-medium">Obrador Central</div>
<div className="text-sm text-gray-500">Múltiples puntos de venta</div>
</button>
</div>
</div>
</div>
</div>
</div>
);
case 2:
return (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-4">
Datos Históricos
</h3>
<p className="text-gray-600 mb-6">
Para obtener predicciones precisas, necesitamos tus datos históricos de ventas.
Puedes subir archivos en varios formatos.
</p>
<div className="bg-blue-50 border border-blue-200 p-4 rounded-lg mb-6">
<div className="flex items-start">
<div className="flex-shrink-0">
<div className="w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center">
<span className="text-white text-sm font-bold">!</span>
</div>
</div>
<div className="ml-3">
<h4 className="text-sm font-medium text-blue-900">
Formatos soportados y estructura de datos
</h4>
<div className="mt-2 text-sm text-blue-700">
<p className="mb-3"><strong>Formatos aceptados:</strong></p>
<div className="grid grid-cols-2 gap-4 mb-3">
<div>
<p className="font-medium">📊 Hojas de cálculo:</p>
<ul className="list-disc list-inside text-xs space-y-1">
<li>.xlsx (Excel moderno)</li>
<li>.xls (Excel clásico)</li>
</ul>
</div>
<div>
<p className="font-medium">📄 Datos estructurados:</p>
<ul className="list-disc list-inside text-xs space-y-1">
<li>.csv (Valores separados por comas)</li>
<li>.json (Formato JSON)</li>
</ul>
</div>
</div>
<p className="mb-2"><strong>Columnas requeridas (en cualquier idioma):</strong></p>
<ul className="list-disc list-inside text-xs space-y-1">
<li><strong>Fecha</strong>: fecha, date, datum (formato: YYYY-MM-DD, DD/MM/YYYY, etc.)</li>
<li><strong>Producto</strong>: producto, product, item, articulo, nombre</li>
<li><strong>Cantidad</strong>: cantidad, quantity, cantidad_vendida, qty</li>
</ul>
</div>
</div>
</div>
</div>
<div className="mt-6 p-6 border-2 border-dashed border-gray-300 rounded-xl hover:border-primary-300 transition-colors">
<div className="text-center">
<Upload className="mx-auto h-12 w-12 text-gray-400" />
<div className="mt-4">
<label htmlFor="sales-file-upload" className="cursor-pointer">
<span className="mt-2 block text-sm font-medium text-gray-900">
Subir archivo de datos históricos
</span>
<span className="mt-1 block text-sm text-gray-500">
Arrastra y suelta tu archivo aquí, o haz clic para seleccionar
</span>
<span className="mt-1 block text-xs text-gray-400">
Máximo 10MB - CSV, Excel (.xlsx, .xls), JSON
</span>
</label>
<input
id="sales-file-upload"
type="file"
accept=".csv,.xlsx,.xls,.json"
required
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
// Validate file size (10MB limit)
const maxSize = 10 * 1024 * 1024;
if (file.size > maxSize) {
toast.error('El archivo es demasiado grande. Máximo 10MB.');
return;
}
setBakeryData(prev => ({
...prev,
csvFile: file,
hasHistoricalData: true
}));
toast.success(`Archivo ${file.name} seleccionado correctamente`);
}
}}
className="hidden"
/>
</div>
</div>
{bakeryData.csvFile ? (
<div className="mt-4 p-4 bg-green-50 rounded-lg">
<div className="flex items-center justify-between">
<div className="flex items-center">
<CheckCircle className="h-5 w-5 text-green-500 mr-2" />
<div>
<p className="text-sm font-medium text-green-700">
{bakeryData.csvFile.name}
</p>
<p className="text-xs text-green-600">
{(bakeryData.csvFile.size / 1024).toFixed(1)} KB {bakeryData.csvFile.type || 'Archivo de datos'}
</p>
</div>
</div>
<button
onClick={() => setBakeryData(prev => ({ ...prev, csvFile: undefined }))}
className="text-red-600 hover:text-red-800 text-sm"
>
Quitar
</button>
</div>
</div>
) : (
<div className="mt-4 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
<div className="flex items-center">
<AlertTriangle className="h-5 w-5 text-yellow-600 mr-2" />
<p className="text-sm text-yellow-800">
<strong>Archivo requerido:</strong> Selecciona un archivo con tus datos históricos de ventas
</p>
</div>
</div>
)}
</div>
{/* Sample formats examples */}
<div className="mt-6 space-y-4">
<h5 className="text-sm font-medium text-gray-900">
Ejemplos de formato:
</h5>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* CSV Example */}
<div className="bg-gray-50 p-4 rounded-lg">
<div className="flex items-center mb-2">
<span className="text-sm font-medium text-gray-900">📄 CSV</span>
</div>
<div className="bg-white p-3 rounded border text-xs font-mono">
<div className="text-gray-600">fecha,producto,cantidad</div>
<div>2024-01-15,Croissants,45</div>
<div>2024-01-15,Pan de molde,32</div>
<div>2024-01-16,Baguettes,28</div>
</div>
</div>
{/* Excel Example */}
<div className="bg-gray-50 p-4 rounded-lg">
<div className="flex items-center mb-2">
<span className="text-sm font-medium text-gray-900">📊 Excel</span>
</div>
<div className="bg-white p-3 rounded border text-xs">
<table className="w-full">
<thead>
<tr className="bg-gray-100">
<th className="p-1 text-left">Fecha</th>
<th className="p-1 text-left">Producto</th>
<th className="p-1 text-left">Cantidad</th>
</tr>
</thead>
<tbody>
<tr><td className="p-1">15/01/2024</td><td className="p-1">Croissants</td><td className="p-1">45</td></tr>
<tr><td className="p-1">15/01/2024</td><td className="p-1">Pan molde</td><td className="p-1">32</td></tr>
</tbody>
</table>
</div>
</div>
</div>
{/* JSON Example */}
<div className="bg-gray-50 p-4 rounded-lg">
<div className="flex items-center mb-2">
<span className="text-sm font-medium text-gray-900">🔧 JSON</span>
</div>
<div className="bg-white p-3 rounded border text-xs font-mono">
<div className="text-gray-600">[</div>
<div className="ml-2">{"{"}"fecha": "2024-01-15", "producto": "Croissants", "cantidad": 45{"}"},</div>
<div className="ml-2">{"{"}"fecha": "2024-01-15", "producto": "Pan de molde", "cantidad": 32{"}"}</div>
<div className="text-gray-600">]</div>
</div>
</div>
</div>
</div>
</div>
);
case 3:
return (
<EnhancedTrainingProgress
progress={{
progress: trainingProgress.progress,
status: trainingProgress.status,
currentStep: trainingProgress.currentStep,
productsCompleted: trainingProgress.productsCompleted,
productsTotal: trainingProgress.productsTotal,
estimatedTimeRemaining: trainingProgress.estimatedTimeRemaining,
error: trainingProgress.error
}}
onTimeout={() => {
// 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 4:
return (
<div className="text-center space-y-6">
<div className="mx-auto w-16 h-16 bg-green-100 rounded-full flex items-center justify-center">
<Check className="h-8 w-8 text-green-600" />
</div>
<div>
<h3 className="text-xl font-semibold text-gray-900 mb-2">
¡Configuración Completada! 🎉
</h3>
<p className="text-gray-600">
Tu panadería está lista para usar PanIA. Comenzarás a recibir predicciones precisas de demanda.
</p>
</div>
<div className="bg-gradient-to-r from-primary-50 to-blue-50 p-6 rounded-xl">
<h4 className="font-medium text-gray-900 mb-3">Resumen de configuración:</h4>
<div className="text-left space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-600">Panadería:</span>
<span className="font-medium">{bakeryData.name}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Productos:</span>
<span className="font-medium">{bakeryData.products.length} seleccionados</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Datos históricos:</span>
<span className="font-medium text-green-600">
{bakeryData.csvFile?.name.split('.').pop()?.toUpperCase()} subido
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Modelo IA:</span>
<span className="font-medium text-green-600"> Entrenado</span>
</div>
</div>
</div>
<div className="bg-yellow-50 border border-yellow-200 p-4 rounded-lg">
<p className="text-sm text-yellow-800">
💡 <strong>Próximo paso:</strong> Explora tu dashboard para ver las primeras predicciones y configurar alertas personalizadas.
</p>
</div>
</div>
);
default:
return null;
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-primary-50 via-white to-blue-50 py-8">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">
Configuración de PanIA
</h1>
<p className="text-gray-600">
Configuremos tu panadería para obtener predicciones precisas de demanda
</p>
</div>
{/* Progress Indicator */}
<div className="mb-8">
<div className="flex justify-center items-center space-x-4 mb-6">
{steps.map((step, index) => (
<div key={step.id} className="flex flex-col items-center">
<div
className={`w-10 h-10 rounded-full flex items-center justify-center border-2 transition-all duration-300 ${
currentStep > step.id
? 'bg-green-500 border-green-500 text-white'
: currentStep === step.id
? 'bg-primary-500 border-primary-500 text-white'
: 'border-gray-300 text-gray-500'
}`}
>
{currentStep > step.id ? (
<Check className="h-5 w-5" />
) : (
<step.icon className="h-5 w-5" />
)}
</div>
<span className="mt-2 text-xs text-gray-500 text-center max-w-20">
{step.title}
</span>
</div>
))}
</div>
<div className="mt-4 bg-gray-200 rounded-full h-2">
<div
className="bg-primary-500 h-2 rounded-full transition-all duration-500"
style={{ width: `${(currentStep / steps.length) * 100}%` }}
/>
</div>
</div>
{/* Step Content */}
<div className="bg-white rounded-2xl shadow-soft p-6 mb-8">
{renderStep()}
</div>
{/* Navigation */}
<div className="flex justify-between">
<button
onClick={handlePrevious}
disabled={currentStep === 1}
className="flex items-center px-4 py-2 text-gray-600 disabled:text-gray-400 disabled:cursor-not-allowed hover:text-gray-800 transition-colors"
>
<ChevronLeft className="h-5 w-5 mr-1" />
Anterior
</button>
{/* Dynamic Next/Complete Button */}
{currentStep < 3 ? (
<button
onClick={handleNext}
disabled={isLoading}
className="flex items-center px-6 py-2 bg-primary-500 text-white rounded-xl hover:bg-primary-600 transition-all hover:shadow-lg disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? (
<>
<Loader className="h-5 w-5 mr-2 animate-spin" />
Procesando...
</>
) : (
<>
Siguiente
<ChevronRight className="h-5 w-5 ml-1" />
</>
)}
</button>
) : currentStep === 3 ? (
// Training step - show different buttons based on status
<div className="flex space-x-3">
{trainingProgress.status === 'failed' ? (
<>
<button
onClick={handleRetryTraining}
disabled={isLoading}
className="flex items-center px-4 py-2 bg-primary-500 text-white rounded-xl hover:bg-primary-600 transition-all disabled:opacity-50"
>
{isLoading ? (
<Loader className="h-5 w-5 mr-2 animate-spin" />
) : (
<Brain className="h-5 w-5 mr-2" />
)}
Reintentar
</button>
<button
onClick={handleSkipTraining}
className="flex items-center px-4 py-2 border border-gray-300 text-gray-700 rounded-xl hover:bg-gray-50 transition-all"
>
Continuar sin entrenar
<ChevronRight className="h-5 w-5 ml-1" />
</button>
</>
) : trainingProgress.status === 'completed' ? (
<button
onClick={() => setCurrentStep(4)}
className="flex items-center px-6 py-2 bg-green-500 text-white rounded-xl hover:bg-green-600 transition-all hover:shadow-lg"
>
Continuar
<ChevronRight className="h-5 w-5 ml-1" />
</button>
) : (
// Training in progress - show status
<button
disabled
className="flex items-center px-6 py-2 bg-gray-400 text-white rounded-xl cursor-not-allowed"
>
<Loader className="h-5 w-5 mr-2 animate-spin" />
Entrenando... {trainingProgress.progress}%
</button>
)}
</div>
) : (
// Final step - Complete button
<button
onClick={handleComplete}
disabled={isLoading}
className="flex items-center px-6 py-2 bg-success-500 text-white rounded-xl hover:bg-success-600 transition-all hover:shadow-lg disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? (
<>
<Loader className="h-5 w-5 mr-2 animate-spin" />
Completando...
</>
) : (
<>
<Check className="h-5 w-5 mr-2" />
Ir al Dashboard
</>
)}
</button>
)}
</div>
{/* Help Section */}
<div className="mt-8 text-center">
<div className="bg-white rounded-xl p-6 shadow-sm border border-gray-100">
<h4 className="font-medium text-gray-900 mb-3">¿Necesitas ayuda?</h4>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
<div className="flex items-center justify-center space-x-2 text-gray-600">
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
<span>📧 soporte@pania.es</span>
</div>
<div className="flex items-center justify-center space-x-2 text-gray-600">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span>📞 +34 900 123 456</span>
</div>
<div className="flex items-center justify-center space-x-2 text-gray-600">
<div className="w-2 h-2 bg-purple-500 rounded-full"></div>
<span>💬 Chat en vivo</span>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default OnboardingPage;