2025-09-08 22:28:26 +02:00
import React , { useState , useCallback , useEffect } from 'react' ;
2025-10-15 16:12:49 +02:00
import { useNavigate } from 'react-router-dom' ;
import { useTranslation } from 'react-i18next' ;
2025-09-08 17:19:00 +02:00
import { Button } from '../../../ui/Button' ;
import { useCurrentTenant } from '../../../../stores/tenant.store' ;
2025-09-29 07:54:25 +02:00
import { useCreateTrainingJob , useTrainingWebSocket , useTrainingJobStatus } from '../../../../api/hooks/training' ;
2025-10-15 16:12:49 +02:00
import { Info } from 'lucide-react' ;
2025-09-08 17:19:00 +02:00
interface MLTrainingStepProps {
onNext : ( ) = > void ;
onPrevious : ( ) = > void ;
onComplete : ( data? : any ) = > void ;
isFirstStep : boolean ;
isLastStep : boolean ;
2025-09-03 14:06:38 +02:00
}
2025-09-08 17:19:00 +02:00
interface TrainingProgress {
stage : string ;
2025-09-04 18:59:56 +02:00
progress : number ;
2025-09-08 17:19:00 +02:00
message : string ;
currentStep? : string ;
estimatedTimeRemaining? : number ;
2025-10-15 21:09:42 +02:00
estimatedCompletionTime? : string ;
2025-09-04 18:59:56 +02:00
}
2025-09-03 14:06:38 +02:00
2025-09-08 17:19:00 +02:00
export const MLTrainingStep : React.FC < MLTrainingStepProps > = ( {
2025-09-08 22:28:26 +02:00
onComplete
2025-09-03 14:06:38 +02:00
} ) = > {
2025-10-15 16:12:49 +02:00
const { t } = useTranslation ( ) ;
const navigate = useNavigate ( ) ;
2025-09-08 17:19:00 +02:00
const [ trainingProgress , setTrainingProgress ] = useState < TrainingProgress | null > ( null ) ;
const [ isTraining , setIsTraining ] = useState ( false ) ;
const [ error , setError ] = useState < string > ( '' ) ;
const [ jobId , setJobId ] = useState < string | null > ( null ) ;
2025-10-15 16:12:49 +02:00
const [ trainingStartTime , setTrainingStartTime ] = useState < number | null > ( null ) ;
const [ showSkipOption , setShowSkipOption ] = useState ( false ) ;
2025-09-03 14:06:38 +02:00
2025-09-08 17:19:00 +02:00
const currentTenant = useCurrentTenant ( ) ;
const createTrainingJob = useCreateTrainingJob ( ) ;
2025-10-15 16:12:49 +02:00
// Check if training has been running for more than 2 minutes
useEffect ( ( ) = > {
if ( trainingStartTime && isTraining && ! showSkipOption ) {
const checkTimer = setInterval ( ( ) = > {
const elapsedTime = ( Date . now ( ) - trainingStartTime ) / 1000 ; // in seconds
if ( elapsedTime > 120 ) { // 2 minutes
setShowSkipOption ( true ) ;
clearInterval ( checkTimer ) ;
}
} , 5000 ) ; // Check every 5 seconds
return ( ) = > clearInterval ( checkTimer ) ;
}
} , [ trainingStartTime , isTraining , showSkipOption ] ) ;
2025-09-08 21:44:04 +02:00
// Memoized WebSocket callbacks to prevent reconnections
const handleProgress = useCallback ( ( data : any ) = > {
setTrainingProgress ( {
stage : 'training' ,
progress : data.data?.progress || 0 ,
message : data.data?.message || 'Entrenando modelo...' ,
currentStep : data.data?.current_step ,
2025-10-15 21:09:42 +02:00
estimatedTimeRemaining : data.data?.estimated_time_remaining_seconds || data . data ? . estimated_time_remaining ,
estimatedCompletionTime : data.data?.estimated_completion_time
2025-09-08 21:44:04 +02:00
} ) ;
} , [ ] ) ;
const handleCompleted = useCallback ( ( _data : any ) = > {
setTrainingProgress ( {
stage : 'completed' ,
progress : 100 ,
message : 'Entrenamiento completado exitosamente'
} ) ;
setIsTraining ( false ) ;
setTimeout ( ( ) = > {
onComplete ( {
jobId : jobId ,
success : true ,
message : 'Modelo entrenado correctamente'
} ) ;
} , 2000 ) ;
} , [ onComplete , jobId ] ) ;
const handleError = useCallback ( ( data : any ) = > {
setError ( data . data ? . error || data . error || 'Error durante el entrenamiento' ) ;
setIsTraining ( false ) ;
setTrainingProgress ( null ) ;
} , [ ] ) ;
const handleStarted = useCallback ( ( _data : any ) = > {
setTrainingProgress ( {
stage : 'starting' ,
progress : 5 ,
message : 'Iniciando entrenamiento del modelo...'
} ) ;
} , [ ] ) ;
// WebSocket for real-time training progress - only connect when we have a jobId
const { isConnected , connectionError } = useTrainingWebSocket (
2025-09-08 17:19:00 +02:00
currentTenant ? . id || '' ,
jobId || '' ,
undefined , // token will be handled by the service
2025-09-08 21:44:04 +02:00
jobId ? {
onProgress : handleProgress ,
onCompleted : handleCompleted ,
onError : handleError ,
onStarted : handleStarted
} : undefined
2025-09-08 17:19:00 +02:00
) ;
2025-09-04 18:59:56 +02:00
2025-09-29 07:54:25 +02:00
// Smart fallback polling - automatically disabled when WebSocket is connected
const { data : jobStatus } = useTrainingJobStatus (
currentTenant ? . id || '' ,
jobId || '' ,
{
enabled : ! ! jobId && ! ! currentTenant ? . id ,
isWebSocketConnected : isConnected , // This will disable HTTP polling when WebSocket is connected
}
) ;
2025-10-09 14:11:02 +02:00
// Handle training status updates from React Query cache (updated by WebSocket or HTTP fallback)
2025-09-29 07:54:25 +02:00
useEffect ( ( ) = > {
if ( ! jobStatus || ! jobId || trainingProgress ? . stage === 'completed' ) {
return ;
}
2025-10-09 14:11:02 +02:00
console . log ( '📊 Training status update from cache:' , jobStatus ,
` (source: ${ isConnected ? 'WebSocket' : 'HTTP polling' } ) ` ) ;
2025-09-29 07:54:25 +02:00
2025-10-09 14:11:02 +02:00
// Check if training completed
2025-09-29 07:54:25 +02:00
if ( jobStatus . status === 'completed' && trainingProgress ? . stage !== 'completed' ) {
2025-10-09 14:11:02 +02:00
console . log ( ` ✅ Training completion detected (source: ${ isConnected ? 'WebSocket' : 'HTTP polling' } ) ` ) ;
2025-09-29 07:54:25 +02:00
setTrainingProgress ( {
stage : 'completed' ,
progress : 100 ,
2025-10-09 14:11:02 +02:00
message : isConnected
? 'Entrenamiento completado exitosamente'
: 'Entrenamiento completado exitosamente (detectado por verificación HTTP)'
2025-09-29 07:54:25 +02:00
} ) ;
setIsTraining ( false ) ;
setTimeout ( ( ) = > {
onComplete ( {
jobId : jobId ,
success : true ,
message : 'Modelo entrenado correctamente' ,
detectedViaPolling : true
} ) ;
} , 2000 ) ;
} else if ( jobStatus . status === 'failed' ) {
2025-10-09 14:11:02 +02:00
console . log ( ` ❌ Training failure detected (source: ${ isConnected ? 'WebSocket' : 'HTTP polling' } ) ` ) ;
2025-09-29 07:54:25 +02:00
setError ( 'Error detectado durante el entrenamiento (verificación de estado)' ) ;
setIsTraining ( false ) ;
setTrainingProgress ( null ) ;
} else if ( jobStatus . status === 'running' && jobStatus . progress !== undefined ) {
2025-10-09 14:11:02 +02:00
// Update progress if we have newer information
2025-09-29 07:54:25 +02:00
const currentProgress = trainingProgress ? . progress || 0 ;
if ( jobStatus . progress > currentProgress ) {
2025-10-09 14:11:02 +02:00
console . log ( ` 📈 Progress update (source: ${ isConnected ? 'WebSocket' : 'HTTP polling' } ): ${ jobStatus . progress } % ` ) ;
2025-09-29 07:54:25 +02:00
setTrainingProgress ( prev = > ( {
. . . prev ,
stage : 'training' ,
progress : jobStatus.progress ,
message : jobStatus.message || 'Entrenando modelo...' ,
currentStep : jobStatus.current_step
} ) as TrainingProgress ) ;
}
}
2025-10-09 14:11:02 +02:00
} , [ jobStatus , jobId , trainingProgress ? . stage , onComplete , isConnected ] ) ;
2025-09-29 07:54:25 +02:00
2025-09-08 22:28:26 +02:00
// Auto-trigger training when component mounts
useEffect ( ( ) = > {
if ( currentTenant ? . id && ! isTraining && ! trainingProgress && ! error ) {
console . log ( '🚀 Auto-starting ML training for tenant:' , currentTenant . id ) ;
handleStartTraining ( ) ;
}
} , [ currentTenant ? . id ] ) ; // Only run when tenant is available
2025-09-05 22:46:28 +02:00
const handleStartTraining = async ( ) = > {
2025-09-08 17:19:00 +02:00
if ( ! currentTenant ? . id ) {
setError ( 'No se encontró información del tenant' ) ;
2025-09-04 18:59:56 +02:00
return ;
}
2025-09-08 17:19:00 +02:00
setIsTraining ( true ) ;
setError ( '' ) ;
setTrainingProgress ( {
stage : 'preparing' ,
progress : 0 ,
message : 'Preparando datos para entrenamiento...'
2025-09-05 22:46:28 +02:00
} ) ;
2025-09-04 18:59:56 +02:00
2025-09-08 17:19:00 +02:00
try {
const response = await createTrainingJob . mutateAsync ( {
tenantId : currentTenant.id ,
request : {
// Use the exact backend schema - all fields are optional
// This will train on all available data
}
} ) ;
setJobId ( response . job_id ) ;
2025-10-15 16:12:49 +02:00
setTrainingStartTime ( Date . now ( ) ) ; // Track when training started
2025-09-08 17:19:00 +02:00
setTrainingProgress ( {
stage : 'queued' ,
progress : 10 ,
message : 'Trabajo de entrenamiento en cola...'
} ) ;
} catch ( err ) {
setError ( 'Error al iniciar el entrenamiento del modelo' ) ;
setIsTraining ( false ) ;
setTrainingProgress ( null ) ;
2025-09-03 14:06:38 +02:00
}
} ;
2025-10-15 16:12:49 +02:00
const handleSkipToDashboard = ( ) = > {
// Navigate to dashboard while training continues in background
console . log ( '🚀 User chose to skip to dashboard while training continues' ) ;
navigate ( '/app/dashboard' ) ;
} ;
2025-09-08 17:19:00 +02:00
const formatTime = ( seconds? : number ) = > {
if ( ! seconds ) return '' ;
2025-10-15 21:09:42 +02:00
2025-09-08 17:19:00 +02:00
if ( seconds < 60 ) {
return ` ${ Math . round ( seconds ) } s ` ;
} else if ( seconds < 3600 ) {
return ` ${ Math . round ( seconds / 60 ) } m ` ;
} else {
return ` ${ Math . round ( seconds / 3600 ) } h ${ Math . round ( ( seconds % 3600 ) / 60 ) } m ` ;
2025-09-03 14:06:38 +02:00
}
} ;
2025-10-15 21:09:42 +02:00
const formatEstimatedCompletionTime = ( isoString? : string ) = > {
if ( ! isoString ) return '' ;
try {
const completionDate = new Date ( isoString ) ;
const now = new Date ( ) ;
// If completion is today, show time only
if ( completionDate . toDateString ( ) === now . toDateString ( ) ) {
return completionDate . toLocaleTimeString ( 'es-ES' , {
hour : '2-digit' ,
minute : '2-digit'
} ) ;
}
// If completion is another day, show date and time
return completionDate . toLocaleString ( 'es-ES' , {
month : 'short' ,
day : 'numeric' ,
hour : '2-digit' ,
minute : '2-digit'
} ) ;
} catch ( error ) {
return '' ;
}
} ;
2025-09-03 14:06:38 +02:00
return (
2025-09-08 17:19:00 +02:00
< div className = "space-y-6" >
2025-09-04 18:59:56 +02:00
< div className = "text-center" >
2025-09-08 17:19:00 +02:00
< p className = "text-[var(--text-secondary)] mb-6" >
2025-09-08 22:28:26 +02:00
Perfecto ! Ahora entrenaremos automáticamente tu modelo de inteligencia artificial utilizando los datos de ventas
2025-09-08 17:19:00 +02:00
e inventario que has proporcionado . Este proceso puede tomar varios minutos .
2025-09-03 14:06:38 +02:00
< / p >
< / div >
2025-09-08 17:19:00 +02:00
{ /* Training Status Card */ }
< div className = "bg-[var(--bg-secondary)] rounded-lg p-6" >
< div className = "text-center" >
{ ! isTraining && ! trainingProgress && (
< div className = "space-y-4" >
< div className = "mx-auto w-16 h-16 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center" >
2025-09-08 22:28:26 +02:00
< div className = "animate-spin rounded-full h-8 w-8 border-b-2 border-[var(--color-primary)]" > < / div >
2025-09-08 17:19:00 +02:00
< / div >
< div >
2025-09-08 22:28:26 +02:00
< h3 className = "text-lg font-semibold mb-2" > Iniciando Entrenamiento Automático < / h3 >
2025-09-08 17:19:00 +02:00
< p className = "text-[var(--text-secondary)] text-sm" >
2025-09-08 22:28:26 +02:00
Preparando el entrenamiento de tu modelo con los datos proporcionados . . .
2025-09-08 17:19:00 +02:00
< / p >
< / div >
< / div >
) }
{ trainingProgress && (
< div className = "space-y-4" >
< div className = "mx-auto w-16 h-16 relative" >
{ trainingProgress . stage === 'completed' ? (
< div className = "w-16 h-16 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center" >
< svg className = "w-8 h-8 text-[var(--color-success)]" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M5 13l4 4L19 7" / >
< / svg >
< / div >
) : (
< div className = "w-16 h-16 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center" >
< div className = "animate-spin rounded-full h-8 w-8 border-b-2 border-[var(--color-primary)]" > < / div >
< / div >
) }
{ trainingProgress . progress > 0 && trainingProgress . stage !== 'completed' && (
< div className = "absolute -bottom-2 left-1/2 transform -translate-x-1/2" >
< span className = "text-xs font-medium text-[var(--text-tertiary)]" >
{ trainingProgress . progress } %
< / span >
< / div >
) }
< / div >
2025-09-03 14:06:38 +02:00
2025-09-08 17:19:00 +02:00
< div >
< h3 className = "text-lg font-semibold mb-2" >
{ trainingProgress . stage === 'completed'
? '¡Entrenamiento Completo!'
: 'Entrenando Modelo IA'
}
< / h3 >
< p className = "text-[var(--text-secondary)] text-sm mb-4" >
{ trainingProgress . message }
< / p >
{ trainingProgress . stage !== 'completed' && (
2025-10-15 21:09:42 +02:00
< div className = "space-y-3" >
{ /* Enhanced Progress Bar */ }
< div className = "relative" >
< div className = "w-full bg-[var(--bg-tertiary)] rounded-full h-3 overflow-hidden" >
< div
className = "bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-primary)]/80 h-3 rounded-full transition-all duration-500 ease-out relative"
style = { { width : ` ${ trainingProgress . progress } % ` } }
>
{ /* Animated shimmer effect */ }
< div className = "absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent animate-shimmer" > < / div >
< / div >
< / div >
{ /* Progress percentage badge */ }
< div className = "absolute -top-1 left-1/2 transform -translate-x-1/2 -translate-y-full mb-1" >
< span className = "text-xs font-semibold text-[var(--color-primary)] bg-[var(--bg-primary)] px-2 py-1 rounded-full shadow-sm border border-[var(--color-primary)]/20" >
{ trainingProgress . progress } %
< / span >
< / div >
2025-09-08 17:19:00 +02:00
< / div >
2025-10-15 21:09:42 +02:00
{ /* Training Information */ }
< div className = "flex flex-col gap-2 text-xs text-[var(--text-tertiary)]" >
{ /* Current Step */ }
< div className = "flex justify-between items-center" >
< span className = "font-medium" > { trainingProgress . currentStep || t ( 'onboarding:steps.ml_training.progress.data_preparation' , 'Procesando...' ) } < / span >
2025-09-08 21:44:04 +02:00
{ jobId && (
2025-10-15 21:09:42 +02:00
< span className = { ` text-xs px-2 py-0.5 rounded-full ${ isConnected ? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400' : 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400' } ` } >
{ isConnected ? '● En vivo' : '● Reconectando...' }
2025-09-08 21:44:04 +02:00
< / span >
) }
2025-10-15 21:09:42 +02:00
< / div >
{ /* Time Information */ }
< div className = "flex flex-wrap gap-x-4 gap-y-1 text-xs" >
2025-09-08 21:44:04 +02:00
{ trainingProgress . estimatedTimeRemaining && (
2025-10-15 21:09:42 +02:00
< div className = "flex items-center gap-1" >
< svg className = "w-3.5 h-3.5" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" / >
< / svg >
< span >
{ t ( 'onboarding:steps.ml_training.estimated_time_remaining' , 'Tiempo restante: {{time}}' , {
time : formatTime ( trainingProgress . estimatedTimeRemaining )
} ) }
< / span >
< / div >
) }
{ trainingProgress . estimatedCompletionTime && (
< div className = "flex items-center gap-1" >
< svg className = "w-3.5 h-3.5" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" / >
< / svg >
< span >
Finalizará : { formatEstimatedCompletionTime ( trainingProgress . estimatedCompletionTime ) }
< / span >
< / div >
2025-09-08 21:44:04 +02:00
) }
< / div >
2025-09-08 17:19:00 +02:00
< / div >
< / div >
) }
2025-09-03 14:06:38 +02:00
< / div >
2025-09-08 17:19:00 +02:00
< / div >
2025-09-04 18:59:56 +02:00
) }
2025-09-03 14:06:38 +02:00
< / div >
2025-09-08 17:19:00 +02:00
< / div >
2025-09-03 14:06:38 +02:00
2025-10-15 16:12:49 +02:00
{ /* Skip to Dashboard Option - Show after 2 minutes */ }
{ showSkipOption && isTraining && trainingProgress ? . stage !== 'completed' && (
< div className = "bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4" >
< div className = "flex items-start gap-3" >
< div className = "flex-shrink-0 mt-0.5" >
< Info className = "w-5 h-5 text-blue-600 dark:text-blue-400" / >
< / div >
< div className = "flex-1" >
< h4 className = "font-medium text-blue-900 dark:text-blue-100 mb-1" >
{ t ( 'onboarding:steps.ml_training.skip_to_dashboard.title' , '¿Toma demasiado tiempo?' ) }
< / h4 >
< p className = "text-sm text-blue-800 dark:text-blue-200 mb-3" >
{ t ( 'onboarding:steps.ml_training.skip_to_dashboard.info' , 'El entrenamiento está tardando más de lo esperado. No te preocupes, puedes explorar tu dashboard mientras el modelo termina de entrenarse en segundo plano.' ) }
< / p >
< Button
onClick = { handleSkipToDashboard }
variant = "secondary"
size = "sm"
>
{ t ( 'onboarding:steps.ml_training.skip_to_dashboard.button' , 'Ir al Dashboard' ) }
< / Button >
< p className = "text-xs text-blue-700 dark:text-blue-300 mt-2" >
{ t ( 'onboarding:steps.ml_training.skip_to_dashboard.training_continues' , 'El entrenamiento continúa en segundo plano' ) }
< / p >
< / div >
< / div >
< / div >
) }
2025-09-08 17:19:00 +02:00
{ /* Training Info */ }
< div className = "bg-[var(--bg-secondary)] rounded-lg p-4" >
< h4 className = "font-medium mb-2" > ¿ Qué sucede durante el entrenamiento ? < / h4 >
< ul className = "text-sm text-[var(--text-secondary)] space-y-1" >
< li > • Análisis de patrones de ventas históricos < / li >
< li > • Creación de modelos predictivos de demanda < / li >
< li > • Optimización de algoritmos de inventario < / li >
< li > • Validación y ajuste de precisión < / li >
< / ul >
< / div >
2025-09-03 14:06:38 +02:00
2025-09-08 21:44:04 +02:00
{ ( error || connectionError ) && (
2025-09-08 17:19:00 +02:00
< div className = "bg-[var(--color-error)]/10 border border-[var(--color-error)]/20 rounded-lg p-4" >
2025-09-08 21:44:04 +02:00
< p className = "text-[var(--color-error)]" > { error || connectionError } < / p >
2025-09-08 17:19:00 +02:00
< / div >
2025-09-04 18:59:56 +02:00
) }
2025-09-03 14:06:38 +02:00
2025-09-08 22:28:26 +02:00
{ /* Auto-completion when training finishes - no manual buttons needed */ }
2025-09-03 14:06:38 +02:00
< / div >
) ;
} ;