Improve the UX and UI of the onboarding steps

This commit is contained in:
Urtzi Alfaro
2025-08-11 07:50:24 +02:00
parent c4d4aeb449
commit 652a850d0f
4 changed files with 703 additions and 408 deletions

View File

@@ -0,0 +1,372 @@
import React, { useState, useEffect } from 'react';
import {
Sparkles, CheckCircle, Clock, ArrowRight, Coffee,
TrendingUp, Target, Loader, AlertTriangle, Mail,
ChevronDown, ChevronUp, HelpCircle, ExternalLink
} from 'lucide-react';
interface SimplifiedTrainingProgressProps {
progress: {
progress: number;
status: string;
currentStep: string;
productsCompleted: number;
productsTotal: number;
estimatedTimeRemaining: number;
error?: string;
};
onTimeout?: () => void;
onBackgroundMode?: () => void;
onEmailNotification?: (email: string) => void;
}
// Proceso simplificado de entrenamiento en 3 etapas
const TRAINING_STAGES = [
{
id: 'preparing',
title: 'Preparando Tus Datos',
description: 'Estamos organizando tu historial de ventas para encontrar patrones',
userFriendly: 'Piensa en esto como ordenar tus recibos para entender tus días de mayor venta',
progressRange: [0, 30],
icon: Coffee,
color: 'blue',
celebration: '📊 ¡Los datos están listos!'
},
{
id: 'learning',
title: 'Construyendo Tu Modelo',
description: 'Tu IA está aprendiendo de tus patrones de ventas',
userFriendly: 'Como enseñar a un asistente inteligente a reconocer cuándo vendes más pan',
progressRange: [30, 80],
icon: Sparkles,
color: 'purple',
celebration: '🧠 ¡Tu IA se está volviendo inteligente!'
},
{
id: 'finalizing',
title: 'Casi Listo',
description: 'Ajustando las predicciones para tu panadería',
userFriendly: 'Nos aseguramos de que las predicciones funcionen perfectamente para tu negocio específico',
progressRange: [80, 100],
icon: Target,
color: 'green',
celebration: '🎉 ¡Tus predicciones están listas!'
}
];
const BENEFITS_PREVIEW = [
{
icon: TrendingUp,
title: 'Pronósticos Inteligentes',
description: 'Saber exactamente cuánto hornear cada día',
example: 'Nunca te quedes sin croissants los domingos ocupados'
},
{
icon: Target,
title: 'Reducir Desperdicios',
description: 'Deja de hacer demasiado de productos que se venden poco',
example: 'Ahorra dinero horneando las cantidades correctas'
},
{
icon: Sparkles,
title: 'Detectar Tendencias',
description: 'Ve qué productos se están volviendo populares',
example: 'Nota cuando los clientes empiezan a amar tu nueva receta'
}
];
export default function SimplifiedTrainingProgress({
progress,
onTimeout,
onBackgroundMode,
onEmailNotification
}: SimplifiedTrainingProgressProps) {
const [showDetails, setShowDetails] = useState(false);
const [showTimeoutOptions, setShowTimeoutOptions] = useState(false);
const [emailForNotification, setEmailForNotification] = useState('');
const [celebratingStage, setCelebratingStage] = useState<string | null>(null);
const [startTime] = useState(Date.now());
// Show timeout options after 7 minutes for better UX
useEffect(() => {
const timer = setTimeout(() => {
if (progress.status === 'running' && progress.progress < 90) {
setShowTimeoutOptions(true);
}
}, 420000); // 7 minutes
return () => clearTimeout(timer);
}, [progress.status, progress.progress]);
// Celebrate stage completions
useEffect(() => {
TRAINING_STAGES.forEach(stage => {
if (progress.progress >= stage.progressRange[1] &&
celebratingStage !== stage.id &&
progress.progress > 0) {
setCelebratingStage(stage.id);
setTimeout(() => setCelebratingStage(null), 3000);
}
});
}, [progress.progress, celebratingStage]);
const getCurrentStage = () => {
return TRAINING_STAGES.find(stage =>
progress.progress >= stage.progressRange[0] &&
progress.progress < stage.progressRange[1]
) || TRAINING_STAGES[TRAINING_STAGES.length - 1];
};
const formatTimeRemaining = (timeValue: number): string => {
if (!timeValue || timeValue <= 0) return 'Casi terminado';
// Manejar tanto segundos como minutos del backend
// Si el valor es muy grande, probablemente sean segundos; si es pequeño, probablemente sean minutos
const minutes = timeValue > 120 ? Math.floor(timeValue / 60) : Math.floor(timeValue);
if (minutes <= 1) return 'Menos de un minuto';
if (minutes < 60) return `Aproximadamente ${minutes} minutos restantes`;
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
return hours === 1
? `Aproximadamente 1 hora ${remainingMinutes > 0 ? `${remainingMinutes} minutos` : ''} restantes`.trim()
: `Aproximadamente ${hours} horas ${remainingMinutes > 0 ? `${remainingMinutes} minutos` : ''} restantes`.trim();
};
const handleEmailNotification = () => {
if (emailForNotification && onEmailNotification) {
onEmailNotification(emailForNotification);
setShowTimeoutOptions(false);
}
};
const currentStage = getCurrentStage();
// Error State
if (progress.status === 'failed' || progress.error) {
return (
<div className="max-w-3xl mx-auto text-center">
<div className="bg-white rounded-3xl shadow-lg p-8 mb-6">
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
<AlertTriangle className="w-8 h-8 text-red-500" />
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-3">
Algo salió mal
</h2>
<p className="text-gray-600 mb-6">
No te preocupes - esto pasa a veces. Nuestro equipo ha sido notificado y lo arreglará rápidamente.
</p>
<div className="bg-gray-50 rounded-xl p-4 mb-6">
<p className="text-sm text-gray-700">
Puedes intentar iniciar el entrenamiento de nuevo, o contactar a nuestro equipo de soporte si esto sigue pasando.
</p>
</div>
<button
onClick={() => window.location.reload()}
className="bg-primary-500 text-white px-6 py-3 rounded-xl font-medium hover:bg-primary-600 transition-colors"
>
Intentar de Nuevo
</button>
</div>
</div>
);
}
return (
<div className="max-w-4xl mx-auto">
{/* Main Progress Card */}
<div className="bg-white rounded-3xl shadow-lg overflow-hidden mb-6">
{/* Header */}
<div className="bg-gradient-to-r from-primary-50 to-secondary-50 p-8 text-center">
<div className="w-16 h-16 bg-gradient-to-r from-primary-500 to-primary-600 rounded-full flex items-center justify-center mx-auto mb-4">
<currentStage.icon className="w-8 h-8 text-white" />
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-2">
{currentStage.title}
</h2>
<p className="text-lg text-gray-600 mb-4">
{currentStage.description}
</p>
{/* Progress Bar */}
<div className="max-w-md mx-auto">
<div className="flex justify-between text-sm text-gray-500 mb-2">
<span>Progreso</span>
<span>{Math.round(progress.progress)}%</span>
</div>
<div className="bg-gray-200 rounded-full h-3 overflow-hidden">
<div
className="bg-gradient-to-r from-primary-500 to-primary-600 h-full transition-all duration-500 ease-out rounded-full"
style={{ width: `${progress.progress}%` }}
/>
</div>
<div className="mt-2 text-sm text-gray-500">
<Clock className="w-4 h-4 inline mr-1" />
{formatTimeRemaining(progress.estimatedTimeRemaining)}
</div>
</div>
</div>
{/* Stage Progress */}
<div className="p-6">
<div className="flex justify-center space-x-6 mb-6">
{TRAINING_STAGES.map((stage, index) => {
const isCompleted = progress.progress >= stage.progressRange[1];
const isCurrent = stage.id === currentStage.id;
const isCelebrating = celebratingStage === stage.id;
return (
<div key={stage.id} className="flex flex-col items-center relative">
{isCelebrating && (
<div className="absolute -top-8 bg-green-100 text-green-800 text-xs px-2 py-1 rounded-full animate-bounce">
{stage.celebration}
</div>
)}
<div className={`w-12 h-12 rounded-full flex items-center justify-center transition-all duration-500 ${
isCompleted
? 'bg-green-500 text-white shadow-lg scale-110'
: isCurrent
? 'bg-primary-500 text-white animate-pulse shadow-lg'
: 'bg-gray-200 text-gray-500'
}`}>
{isCompleted ? (
<CheckCircle className="w-6 h-6" />
) : (
<stage.icon className="w-6 h-6" />
)}
</div>
<span className="text-xs text-gray-600 mt-2 text-center">
{stage.title.split(' ')[0]}
</span>
{index < TRAINING_STAGES.length - 1 && (
<ArrowRight className={`absolute top-5 -right-3 w-4 h-4 ${
isCompleted ? 'text-green-500' : 'text-gray-300'
}`} />
)}
</div>
);
})}
</div>
{/* Current Stage Explanation */}
<div className="bg-gray-50 rounded-xl p-4 text-center mb-4">
<p className="text-gray-700">
{currentStage.userFriendly}
</p>
</div>
{/* Optional Details */}
<button
onClick={() => setShowDetails(!showDetails)}
className="flex items-center justify-center w-full text-sm text-gray-500 hover:text-gray-700 transition-colors"
>
<HelpCircle className="w-4 h-4 mr-1" />
{showDetails ? 'Ocultar' : 'Aprende más sobre qué está pasando'}
{showDetails ? <ChevronUp className="w-4 h-4 ml-1" /> : <ChevronDown className="w-4 h-4 ml-1" />}
</button>
{showDetails && (
<div className="mt-4 p-4 bg-blue-50 rounded-xl text-sm text-blue-800">
<p className="mb-2">
<strong>Detalles técnicos:</strong> Estamos usando aprendizaje automático para analizar tus patrones de ventas,
tendencias estacionales y relaciones entre productos para crear predicciones precisas de demanda.
</p>
<a
href="#"
className="text-blue-600 hover:text-blue-700 inline-flex items-center"
>
Aprende más sobre nuestra tecnología de IA
<ExternalLink className="w-3 h-3 ml-1" />
</a>
</div>
)}
</div>
</div>
{/* Benefits Preview */}
<div className="bg-white rounded-3xl shadow-lg p-6 mb-6">
<h3 className="text-xl font-bold text-gray-900 mb-4 text-center">
Lo que obtendrás cuando esto termine
</h3>
<div className="grid md:grid-cols-3 gap-4">
{BENEFITS_PREVIEW.map((benefit, index) => (
<div key={index} className="text-center p-4 rounded-xl bg-gradient-to-b from-gray-50 to-white border">
<div className="w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-3">
<benefit.icon className="w-6 h-6 text-blue-600" />
</div>
<h4 className="font-semibold text-gray-900 mb-2">{benefit.title}</h4>
<p className="text-gray-600 text-sm mb-2">{benefit.description}</p>
<p className="text-xs text-blue-600 italic">ej., {benefit.example}</p>
</div>
))}
</div>
</div>
{/* Timeout Options */}
{showTimeoutOptions && (
<div className="bg-yellow-50 border border-yellow-200 rounded-3xl p-6">
<div className="text-center mb-4">
<Clock className="w-8 h-8 text-yellow-600 mx-auto mb-2" />
<h3 className="text-lg font-semibold text-gray-900">¿Tomando más tiempo del esperado?</h3>
<p className="text-gray-600">
¡No te preocupes! Tienes algunas opciones mientras terminamos.
</p>
</div>
<div className="grid md:grid-cols-2 gap-4">
{/* Continue in Background */}
<div className="bg-white rounded-xl p-4">
<h4 className="font-semibold text-gray-900 mb-2">Continuar en segundo plano</h4>
<p className="text-sm text-gray-600 mb-3">
Explora una vista previa de tu panel de control mientras continúa el entrenamiento.
</p>
<button
onClick={onBackgroundMode}
className="w-full bg-primary-500 text-white py-2 px-4 rounded-lg hover:bg-primary-600 transition-colors"
>
Vista Previa del Panel
</button>
</div>
{/* Email Notification */}
<div className="bg-white rounded-xl p-4">
<h4 className="font-semibold text-gray-900 mb-2">Recibir notificación</h4>
<p className="text-sm text-gray-600 mb-3">
Te enviaremos un correo cuando tu modelo esté listo.
</p>
<div className="space-y-2">
<input
type="email"
placeholder="tu@correo.com"
value={emailForNotification}
onChange={(e) => setEmailForNotification(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
/>
<button
onClick={handleEmailNotification}
disabled={!emailForNotification}
className="w-full bg-green-500 text-white py-2 px-4 rounded-lg hover:bg-green-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<Mail className="w-4 h-4 inline mr-1" />
Notificarme
</button>
</div>
</div>
</div>
<div className="text-center mt-4">
<button
onClick={() => setShowTimeoutOptions(false)}
className="text-sm text-gray-500 hover:text-gray-700"
>
Esperaré aquí
</button>
</div>
</div>
)}
</div>
);
}