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

423 lines
15 KiB
TypeScript
Raw Normal View History

import React, { useState, useEffect, useRef } from 'react';
import { Brain, Activity, Zap, CheckCircle, AlertCircle, TrendingUp, Upload, Database } from 'lucide-react';
2025-09-03 14:06:38 +02:00
import { Button, Card, Badge } from '../../../ui';
import { OnboardingStepProps } from '../OnboardingWizard';
import { useOnboarding } from '../../../../hooks/business/onboarding';
import { useAuthUser } from '../../../../stores/auth.store';
import { useCurrentTenant } from '../../../../stores';
// Type definitions for training messages (will be moved to API types later)
interface TrainingProgressMessage {
type: 'training_progress';
progress: number;
stage: string;
message: string;
}
interface TrainingCompletedMessage {
type: 'training_completed';
metrics: TrainingMetrics;
}
interface TrainingErrorMessage {
type: 'training_error';
error: string;
}
2025-09-03 14:06:38 +02:00
interface TrainingMetrics {
accuracy: number;
mape: number;
mae: number;
rmse: number;
2025-09-03 14:06:38 +02:00
}
interface TrainingLog {
timestamp: string;
message: string;
level: 'info' | 'warning' | 'error' | 'success';
}
interface TrainingJob {
id: string;
status: 'pending' | 'running' | 'completed' | 'failed';
progress: number;
started_at?: string;
completed_at?: string;
error_message?: string;
metrics?: TrainingMetrics;
}
2025-09-03 14:06:38 +02:00
// Using the proper training service from services/api/training.service.ts
2025-09-03 14:06:38 +02:00
export const MLTrainingStep: React.FC<OnboardingStepProps> = ({
data,
onDataChange,
onNext,
onPrevious,
isFirstStep,
isLastStep
}) => {
const user = useAuthUser();
const currentTenant = useCurrentTenant();
// Use the onboarding hooks
const {
startTraining,
trainingOrchestration: {
status,
progress,
currentStep,
estimatedTimeRemaining,
job,
logs,
metrics
},
data: allStepData,
isLoading,
error,
clearError
} = useOnboarding();
2025-09-03 14:06:38 +02:00
// Local state for UI-only elements
const [hasStarted, setHasStarted] = useState(false);
const wsRef = useRef<WebSocket | null>(null);
2025-09-03 14:06:38 +02:00
// Validate that required data is available for training
const validateDataRequirements = (): { isValid: boolean; missingItems: string[] } => {
const missingItems: string[] = [];
2025-09-03 14:06:38 +02:00
console.log('MLTrainingStep - Validating data requirements');
console.log('MLTrainingStep - Current allStepData:', allStepData);
// Check if sales data was processed
const hasProcessingResults = allStepData?.processingResults &&
allStepData.processingResults.is_valid &&
allStepData.processingResults.total_records > 0;
// Check if sales data was imported (required for training)
const hasImportResults = allStepData?.salesImportResult &&
(allStepData.salesImportResult.records_created > 0 ||
allStepData.salesImportResult.success === true ||
allStepData.salesImportResult.imported === true);
if (!hasProcessingResults) {
missingItems.push('Datos de ventas validados');
}
// Sales data must be imported for ML training to work
if (!hasImportResults) {
missingItems.push('Datos de ventas importados');
2025-09-03 14:06:38 +02:00
}
// Check if products were approved in review step
const hasApprovedProducts = allStepData?.approvedProducts &&
allStepData.approvedProducts.length > 0 &&
allStepData.reviewCompleted;
2025-09-03 14:06:38 +02:00
if (!hasApprovedProducts) {
missingItems.push('Productos aprobados en revisión');
}
// Check if inventory was configured
const hasInventoryConfig = allStepData?.inventoryConfigured &&
allStepData?.inventoryItems &&
allStepData.inventoryItems.length > 0;
2025-09-03 14:06:38 +02:00
if (!hasInventoryConfig) {
missingItems.push('Inventario configurado');
}
2025-09-03 14:06:38 +02:00
// Check if we have enough data for training
if (dataProcessingData?.processingResults?.total_records &&
dataProcessingData.processingResults.total_records < 10) {
missingItems.push('Suficientes registros de ventas (mínimo 10)');
2025-09-03 14:06:38 +02:00
}
console.log('MLTrainingStep - Validation result:', {
isValid: missingItems.length === 0,
missingItems,
hasProcessingResults,
hasImportResults,
hasApprovedProducts,
hasInventoryConfig
});
2025-09-03 14:06:38 +02:00
return {
isValid: missingItems.length === 0,
missingItems
};
};
const handleStartTraining = async () => {
// Validate data requirements
const validation = validateDataRequirements();
if (!validation.isValid) {
console.error('Datos insuficientes para entrenamiento:', validation.missingItems);
return;
}
setHasStarted(true);
// Use the onboarding hook for training
const success = await startTraining({
// You can pass options here if needed
startDate: allStepData?.processingResults?.summary?.date_range?.split(' - ')[0],
endDate: allStepData?.processingResults?.summary?.date_range?.split(' - ')[1],
});
if (!success) {
console.error('Error starting training');
setHasStarted(false);
2025-09-03 14:06:38 +02:00
}
2025-09-03 14:06:38 +02:00
};
// Cleanup WebSocket on unmount
useEffect(() => {
return () => {
if (wsRef.current) {
wsRef.current.disconnect();
wsRef.current = null;
}
};
}, []);
useEffect(() => {
// Auto-start training if all requirements are met and not already started
const validation = validateDataRequirements();
console.log('MLTrainingStep - useEffect validation:', validation);
if (validation.isValid && status === 'idle' && data.autoStartTraining) {
console.log('MLTrainingStep - Auto-starting training...');
// Auto-start after a brief delay to allow user to see the step
const timer = setTimeout(() => {
handleStartTraining();
}, 1000);
return () => clearTimeout(timer);
}
}, [allStepData, data.autoStartTraining, status]);
const getStatusIcon = () => {
switch (status) {
case 'idle': return <Brain className="w-8 h-8 text-[var(--color-primary)]" />;
case 'validating': return <Database className="w-8 h-8 text-[var(--color-info)] animate-pulse" />;
case 'training': return <Activity className="w-8 h-8 text-[var(--color-info)] animate-pulse" />;
case 'completed': return <CheckCircle className="w-8 h-8 text-[var(--color-success)]" />;
case 'failed': return <AlertCircle className="w-8 h-8 text-[var(--color-error)]" />;
default: return <Brain className="w-8 h-8 text-[var(--color-primary)]" />;
}
2025-09-03 14:06:38 +02:00
};
const getStatusColor = () => {
switch (status) {
case 'completed': return 'text-[var(--color-success)]';
case 'failed': return 'text-[var(--color-error)]';
case 'training':
case 'validating': return 'text-[var(--color-info)]';
default: return 'text-[var(--text-primary)]';
2025-09-03 14:06:38 +02:00
}
};
const getStatusMessage = () => {
switch (status) {
case 'idle': return 'Listo para entrenar tu asistente IA';
case 'validating': return 'Validando datos para entrenamiento...';
case 'training': return 'Entrenando modelo de predicción...';
case 'completed': return '¡Tu asistente IA está listo!';
case 'failed': return 'Error en el entrenamiento';
default: return 'Estado desconocido';
2025-09-03 14:06:38 +02:00
}
};
// Check data requirements for display
const validation = validateDataRequirements();
if (!validation.isValid) {
return (
<div className="space-y-8">
<div className="text-center py-16">
<Upload className="w-16 h-16 text-gray-400 mx-auto mb-4" />
<h3 className="text-2xl font-bold text-gray-600 mb-4">
Datos insuficientes para entrenamiento
</h3>
<p className="text-gray-500 mb-6 max-w-md mx-auto">
Para entrenar tu modelo de IA, necesitamos que completes los siguientes elementos:
</p>
<Card className="p-6 max-w-md mx-auto mb-6">
<h4 className="font-semibold mb-4 text-[var(--text-primary)]">Elementos requeridos:</h4>
<ul className="space-y-2 text-left">
{validation.missingItems.map((item, index) => (
<li key={index} className="flex items-center space-x-2">
<AlertCircle className="w-4 h-4 text-red-500" />
<span className="text-sm text-[var(--text-secondary)]">{item}</span>
</li>
))}
</ul>
</Card>
<p className="text-sm text-[var(--text-secondary)] mb-4">
Una vez completados estos elementos, el entrenamiento se iniciará automáticamente.
</p>
<Button
variant="outline"
onClick={onPrevious}
disabled={isFirstStep}
>
Volver al paso anterior
</Button>
</div>
</div>
);
}
2025-09-03 14:06:38 +02:00
return (
<div className="space-y-8">
2025-09-03 14:06:38 +02:00
{/* Header */}
<div className="text-center">
<div className="w-20 h-20 bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-secondary)] rounded-full flex items-center justify-center mx-auto mb-6">
{getStatusIcon()}
2025-09-03 14:06:38 +02:00
</div>
<h2 className={`text-3xl font-bold mb-4 ${getStatusColor()}`}>
Entrenamiento de IA
</h2>
<p className={`text-lg mb-2 ${getStatusColor()}`}>
{getStatusMessage()}
</p>
2025-09-03 14:06:38 +02:00
<p className="text-[var(--text-secondary)]">
Creando tu asistente inteligente personalizado con tus datos de ventas e inventario
2025-09-03 14:06:38 +02:00
</p>
</div>
{/* Progress Bar */}
<Card className="p-6">
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-medium text-[var(--text-primary)]">Progreso del entrenamiento</span>
<span className="text-sm text-[var(--text-secondary)]">{progress.toFixed(1)}%</span>
</div>
{currentStep && (
<div className="mb-2">
<span className="text-xs text-[var(--text-secondary)]">Paso actual: </span>
<span className="text-xs font-medium text-[var(--text-primary)]">{currentStep}</span>
2025-09-03 14:06:38 +02:00
</div>
)}
{estimatedTimeRemaining > 0 && (
<div className="mb-4">
<span className="text-xs text-[var(--text-secondary)]">Tiempo estimado restante: </span>
<span className="text-xs font-medium text-[var(--color-primary)]">
{Math.round(estimatedTimeRemaining / 60)} minutos
</span>
2025-09-03 14:06:38 +02:00
</div>
)}
<div className="w-full bg-[var(--bg-secondary)] rounded-full h-3">
<div
className="bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-secondary)] h-3 rounded-full transition-all duration-300"
style={{ width: `${progress}%` }}
/>
</div>
</Card>
2025-09-03 14:06:38 +02:00
{/* Training Logs */}
<Card className="p-6">
<h3 className="text-lg font-semibold mb-4 flex items-center">
<Activity className="w-5 h-5 mr-2" />
Registro de entrenamiento
</h3>
<div className="space-y-2 max-h-64 overflow-y-auto">
{trainingLogs.length === 0 ? (
<p className="text-[var(--text-secondary)] italic">Esperando inicio de entrenamiento...</p>
) : (
trainingLogs.map((log, index) => (
<div key={index} className="flex items-start space-x-3 p-2 rounded">
<div className={`w-2 h-2 rounded-full mt-2 ${
log.level === 'success' ? 'bg-green-500' :
log.level === 'error' ? 'bg-red-500' :
log.level === 'warning' ? 'bg-yellow-500' :
'bg-blue-500'
}`} />
<div className="flex-1">
<p className="text-sm text-[var(--text-primary)]">{log.message}</p>
<p className="text-xs text-[var(--text-secondary)]">
{new Date(log.timestamp).toLocaleTimeString()}
2025-09-03 14:06:38 +02:00
</p>
</div>
</div>
))
)}
2025-09-03 14:06:38 +02:00
</div>
</Card>
2025-09-03 14:06:38 +02:00
{/* Training Metrics */}
{metrics && status === 'completed' && (
<Card className="p-6">
<h3 className="text-lg font-semibold mb-4 flex items-center">
<TrendingUp className="w-5 h-5 mr-2" />
Métricas del modelo
</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="text-center p-3 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-2xl font-bold text-[var(--color-success)]">
{(metrics.accuracy * 100).toFixed(1)}%
2025-09-03 14:06:38 +02:00
</p>
<p className="text-xs text-[var(--text-secondary)]">Precisión</p>
2025-09-03 14:06:38 +02:00
</div>
<div className="text-center p-3 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-2xl font-bold text-[var(--color-info)]">
{metrics.mape.toFixed(1)}%
</p>
<p className="text-xs text-[var(--text-secondary)]">MAPE</p>
2025-09-03 14:06:38 +02:00
</div>
<div className="text-center p-3 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-2xl font-bold text-[var(--color-primary)]">
{metrics.mae.toFixed(2)}
</p>
<p className="text-xs text-[var(--text-secondary)]">MAE</p>
2025-09-03 14:06:38 +02:00
</div>
<div className="text-center p-3 bg-[var(--bg-secondary)] rounded-lg">
<p className="text-2xl font-bold text-[var(--color-secondary)]">
{metrics.rmse.toFixed(2)}
</p>
<p className="text-xs text-[var(--text-secondary)]">RMSE</p>
2025-09-03 14:06:38 +02:00
</div>
</div>
</Card>
)}
{/* Manual Start Button (if not auto-started) */}
{status === 'idle' && (
<Card className="p-6 text-center">
<Button
onClick={handleStartTraining}
className="bg-[var(--color-primary)] hover:bg-[var(--color-primary)]/90"
size="lg"
>
<Zap className="w-5 h-5 mr-2" />
Iniciar entrenamiento
</Button>
</Card>
)}
2025-09-03 14:06:38 +02:00
{/* Navigation */}
<div className="flex justify-between pt-6 border-t border-[var(--border-primary)]">
<Button
variant="outline"
onClick={onPrevious}
disabled={isFirstStep}
>
Anterior
</Button>
<Button
onClick={onNext}
disabled={trainingStatus !== 'completed'}
className="bg-[var(--color-primary)] hover:bg-[var(--color-primary)]/90"
>
{isLastStep ? 'Finalizar' : 'Siguiente'}
</Button>
</div>
2025-09-03 14:06:38 +02:00
</div>
);
};