Improve the UX and UI of the onboarding steps
This commit is contained in:
372
frontend/src/components/SimplifiedTrainingProgress.tsx
Normal file
372
frontend/src/components/SimplifiedTrainingProgress.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user