2025-09-08 22:28:26 +02:00
|
|
|
import React, { useState, useCallback, useEffect } from 'react';
|
2025-09-08 17:19:00 +02:00
|
|
|
import { Button } from '../../../ui/Button';
|
|
|
|
|
import { useCurrentTenant } from '../../../../stores/tenant.store';
|
|
|
|
|
import { useCreateTrainingJob, useTrainingWebSocket } from '../../../../api/hooks/training';
|
|
|
|
|
|
|
|
|
|
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-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-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-09-03 14:06:38 +02:00
|
|
|
|
2025-09-08 17:19:00 +02:00
|
|
|
const currentTenant = useCurrentTenant();
|
|
|
|
|
const createTrainingJob = useCreateTrainingJob();
|
|
|
|
|
|
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,
|
|
|
|
|
estimatedTimeRemaining: data.data?.estimated_time_remaining
|
|
|
|
|
});
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
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-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);
|
|
|
|
|
|
|
|
|
|
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-09-08 17:19:00 +02:00
|
|
|
const formatTime = (seconds?: number) => {
|
|
|
|
|
if (!seconds) return '';
|
2025-09-04 18:59:56 +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
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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' && (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<div className="w-full bg-[var(--bg-tertiary)] rounded-full h-2">
|
|
|
|
|
<div
|
|
|
|
|
className="bg-[var(--color-primary)] h-2 rounded-full transition-all duration-300"
|
|
|
|
|
style={{ width: `${trainingProgress.progress}%` }}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex justify-between text-xs text-[var(--text-tertiary)]">
|
|
|
|
|
<span>{trainingProgress.currentStep || 'Procesando...'}</span>
|
2025-09-08 21:44:04 +02:00
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
{jobId && (
|
|
|
|
|
<span className={`text-xs ${isConnected ? 'text-green-500' : 'text-red-500'}`}>
|
|
|
|
|
{isConnected ? '🟢 Conectado' : '🔴 Desconectado'}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
{trainingProgress.estimatedTimeRemaining && (
|
|
|
|
|
<span>Tiempo estimado: {formatTime(trainingProgress.estimatedTimeRemaining)}</span>
|
|
|
|
|
)}
|
|
|
|
|
</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-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>
|
|
|
|
|
);
|
|
|
|
|
};
|