Files
bakery-ia/frontend/src/components/domain/onboarding/steps/MLTrainingStep.tsx

265 lines
9.2 KiB
TypeScript
Raw Normal View History

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
}
interface TrainingProgress {
stage: string;
progress: number;
message: string;
currentStep?: string;
estimatedTimeRemaining?: number;
}
2025-09-03 14:06:38 +02:00
export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
2025-09-03 14:06:38 +02:00
onPrevious,
onComplete,
isFirstStep
2025-09-03 14:06:38 +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
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
}
);
const handleStartTraining = async () => {
if (!currentTenant?.id) {
setError('No se encontró información del tenant');
return;
}
setIsTraining(true);
setError('');
setTrainingProgress({
stage: 'preparing',
progress: 0,
message: 'Preparando datos para entrenamiento...'
});
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
}
};
const formatTime = (seconds?: number) => {
if (!seconds) return '';
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 (
<div className="space-y-6">
<div className="text-center">
<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>
{/* 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
<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>
</div>
)}
2025-09-03 14:06:38 +02:00
</div>
</div>
2025-09-03 14:06:38 +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
{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-03 14:06:38 +02:00
{/* Actions */}
<div className="flex justify-between">
<Button
variant="outline"
onClick={onPrevious}
disabled={isFirstStep || isTraining}
>
Anterior
</Button>
{!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>
)}
</div>
2025-09-03 14:06:38 +02:00
</div>
);
};