2025-09-08 17:19:00 +02:00
|
|
|
import React, { useState, useEffect } from 'react';
|
|
|
|
|
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-03 14:06:38 +02:00
|
|
|
onPrevious,
|
2025-09-08 17:19:00 +02:00
|
|
|
onComplete,
|
|
|
|
|
isFirstStep
|
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();
|
|
|
|
|
|
|
|
|
|
// WebSocket for real-time training progress
|
|
|
|
|
const trainingWebSocket = useTrainingWebSocket(
|
|
|
|
|
currentTenant?.id || '',
|
|
|
|
|
jobId || '',
|
|
|
|
|
undefined, // token will be handled by the service
|
|
|
|
|
{
|
|
|
|
|
onProgress: (data) => {
|
|
|
|
|
setTrainingProgress({
|
|
|
|
|
stage: 'training',
|
|
|
|
|
progress: data.progress?.percentage || 0,
|
|
|
|
|
message: data.message || 'Entrenando modelo...',
|
|
|
|
|
currentStep: data.progress?.current_step,
|
|
|
|
|
estimatedTimeRemaining: data.progress?.estimated_time_remaining
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
onCompleted: (data) => {
|
|
|
|
|
setTrainingProgress({
|
|
|
|
|
stage: 'completed',
|
|
|
|
|
progress: 100,
|
|
|
|
|
message: 'Entrenamiento completado exitosamente'
|
|
|
|
|
});
|
|
|
|
|
setIsTraining(false);
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
onComplete({
|
|
|
|
|
jobId: jobId,
|
|
|
|
|
success: true,
|
|
|
|
|
message: 'Modelo entrenado correctamente'
|
|
|
|
|
});
|
|
|
|
|
}, 2000);
|
|
|
|
|
},
|
|
|
|
|
onError: (data) => {
|
|
|
|
|
setError(data.error || 'Error durante el entrenamiento');
|
|
|
|
|
setIsTraining(false);
|
|
|
|
|
setTrainingProgress(null);
|
|
|
|
|
},
|
|
|
|
|
onStarted: (data) => {
|
|
|
|
|
setTrainingProgress({
|
|
|
|
|
stage: 'starting',
|
|
|
|
|
progress: 5,
|
|
|
|
|
message: 'Iniciando entrenamiento del modelo...'
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-09-03 14:06:38 +02:00
|
|
|
}
|
2025-09-08 17:19:00 +02:00
|
|
|
);
|
2025-09-04 18:59:56 +02:00
|
|
|
|
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">
|
|
|
|
|
Ahora entrenaremos tu modelo de inteligencia artificial utilizando los datos de ventas
|
|
|
|
|
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">
|
|
|
|
|
<svg className="w-8 h-8 text-[var(--color-primary)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<h3 className="text-lg font-semibold mb-2">Listo para Entrenar</h3>
|
|
|
|
|
<p className="text-[var(--text-secondary)] text-sm">
|
|
|
|
|
Tu modelo está listo para ser entrenado con los datos proporcionados.
|
|
|
|
|
</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>
|
|
|
|
|
{trainingProgress.estimatedTimeRemaining && (
|
|
|
|
|
<span>Tiempo estimado: {formatTime(trainingProgress.estimatedTimeRemaining)}</span>
|
|
|
|
|
)}
|
|
|
|
|
</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 17:19:00 +02:00
|
|
|
{error && (
|
|
|
|
|
<div className="bg-[var(--color-error)]/10 border border-[var(--color-error)]/20 rounded-lg p-4">
|
|
|
|
|
<p className="text-[var(--color-error)]">{error}</p>
|
|
|
|
|
</div>
|
2025-09-04 18:59:56 +02:00
|
|
|
)}
|
2025-09-03 14:06:38 +02:00
|
|
|
|
2025-09-08 17:19:00 +02:00
|
|
|
{/* Actions */}
|
|
|
|
|
<div className="flex justify-between">
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
2025-09-04 18:59:56 +02:00
|
|
|
onClick={onPrevious}
|
2025-09-08 17:19:00 +02:00
|
|
|
disabled={isFirstStep || isTraining}
|
2025-09-04 18:59:56 +02:00
|
|
|
>
|
|
|
|
|
Anterior
|
|
|
|
|
</Button>
|
2025-09-08 17:19:00 +02:00
|
|
|
|
|
|
|
|
{!isTraining && !trainingProgress && (
|
|
|
|
|
<Button
|
|
|
|
|
onClick={handleStartTraining}
|
|
|
|
|
size="lg"
|
|
|
|
|
disabled={!currentTenant?.id}
|
|
|
|
|
>
|
|
|
|
|
Iniciar Entrenamiento
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{trainingProgress?.stage === 'completed' && (
|
|
|
|
|
<Button
|
|
|
|
|
onClick={() => onComplete()}
|
|
|
|
|
size="lg"
|
|
|
|
|
variant="success"
|
|
|
|
|
>
|
|
|
|
|
Continuar
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
2025-09-04 18:59:56 +02:00
|
|
|
</div>
|
2025-09-03 14:06:38 +02:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|