Start integrating the onboarding flow with backend 7

This commit is contained in:
Urtzi Alfaro
2025-09-05 22:46:28 +02:00
parent 069954981a
commit 548a2ddd11
28 changed files with 5544 additions and 1014 deletions

View File

@@ -2,9 +2,9 @@ import React, { useState, useEffect, useRef } from 'react';
import { Brain, Activity, Zap, CheckCircle, AlertCircle, TrendingUp, Upload, Database } from 'lucide-react';
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/tenant.store';
// TODO: Implement WebSocket training progress updates when realtime API is available
// Type definitions for training messages (will be moved to API types later)
interface TrainingProgressMessage {
@@ -59,50 +59,46 @@ export const MLTrainingStep: React.FC<OnboardingStepProps> = ({
}) => {
const user = useAuthUser();
const currentTenant = useCurrentTenant();
const createAlert = (alert: any) => {
console.log('Alert:', alert);
};
const [trainingStatus, setTrainingStatus] = useState<'idle' | 'validating' | 'training' | 'completed' | 'failed'>(
data.trainingStatus || 'idle'
);
const [progress, setProgress] = useState(data.trainingProgress || 0);
const [currentJob, setCurrentJob] = useState<TrainingJob | null>(data.trainingJob || null);
const [trainingLogs, setTrainingLogs] = useState<TrainingLog[]>(data.trainingLogs || []);
const [metrics, setMetrics] = useState<TrainingMetrics | null>(data.trainingMetrics || null);
const [currentStep, setCurrentStep] = useState<string>('');
const [estimatedTimeRemaining, setEstimatedTimeRemaining] = useState<number>(0);
// Use the onboarding hooks
const {
startTraining,
trainingOrchestration: {
status,
progress,
currentStep,
estimatedTimeRemaining,
job,
logs,
metrics
},
data: allStepData,
isLoading,
error,
clearError
} = useOnboarding();
const wsRef = useRef<WebSocketService | null>(null);
// Local state for UI-only elements
const [hasStarted, setHasStarted] = useState(false);
const wsRef = useRef<WebSocket | null>(null);
// Validate that required data is available for training
const validateDataRequirements = (): { isValid: boolean; missingItems: string[] } => {
const missingItems: string[] = [];
console.log('MLTrainingStep - Validating data requirements');
console.log('MLTrainingStep - Current data:', data);
console.log('MLTrainingStep - allStepData keys:', Object.keys(data.allStepData || {}));
// Get data from previous steps
const dataProcessingData = data.allStepData?.['data-processing'];
const reviewData = data.allStepData?.['review'];
const inventoryData = data.allStepData?.['inventory'];
console.log('MLTrainingStep - dataProcessingData:', dataProcessingData);
console.log('MLTrainingStep - reviewData:', reviewData);
console.log('MLTrainingStep - inventoryData:', inventoryData);
console.log('MLTrainingStep - inventoryData.salesImportResult:', inventoryData?.salesImportResult);
console.log('MLTrainingStep - Current allStepData:', allStepData);
// Check if sales data was processed
const hasProcessingResults = dataProcessingData?.processingResults &&
dataProcessingData.processingResults.is_valid &&
dataProcessingData.processingResults.total_records > 0;
const hasProcessingResults = allStepData?.processingResults &&
allStepData.processingResults.is_valid &&
allStepData.processingResults.total_records > 0;
// Check if sales data was imported (required for training)
const hasImportResults = inventoryData?.salesImportResult &&
(inventoryData.salesImportResult.records_created > 0 ||
inventoryData.salesImportResult.success === true ||
inventoryData.salesImportResult.imported === true);
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');
@@ -114,18 +110,18 @@ export const MLTrainingStep: React.FC<OnboardingStepProps> = ({
}
// Check if products were approved in review step
const hasApprovedProducts = reviewData?.approvedProducts &&
reviewData.approvedProducts.length > 0 &&
reviewData.reviewCompleted;
const hasApprovedProducts = allStepData?.approvedProducts &&
allStepData.approvedProducts.length > 0 &&
allStepData.reviewCompleted;
if (!hasApprovedProducts) {
missingItems.push('Productos aprobados en revisión');
}
// Check if inventory was configured
const hasInventoryConfig = inventoryData?.inventoryConfigured &&
inventoryData?.inventoryItems &&
inventoryData.inventoryItems.length > 0;
const hasInventoryConfig = allStepData?.inventoryConfigured &&
allStepData?.inventoryItems &&
allStepData.inventoryItems.length > 0;
if (!hasInventoryConfig) {
missingItems.push('Inventario configurado');
@@ -152,161 +148,28 @@ export const MLTrainingStep: React.FC<OnboardingStepProps> = ({
};
};
const addLog = (message: string, level: TrainingLog['level'] = 'info') => {
const newLog: TrainingLog = {
timestamp: new Date().toISOString(),
message,
level
};
setTrainingLogs(prev => [...prev, newLog]);
};
const startTraining = async () => {
const tenantId = currentTenant?.id || user?.tenant_id;
if (!tenantId) {
createAlert({
type: 'error',
category: 'system',
priority: 'high',
title: 'Error',
message: 'No se pudo obtener información del tenant',
source: 'onboarding'
});
return;
}
const handleStartTraining = async () => {
// Validate data requirements
const validation = validateDataRequirements();
if (!validation.isValid) {
createAlert({
type: 'error',
category: 'system',
priority: 'high',
title: 'Datos insuficientes para entrenamiento',
message: `Faltan los siguientes elementos: ${validation.missingItems.join(', ')}`,
source: 'onboarding'
});
console.error('Datos insuficientes para entrenamiento:', validation.missingItems);
return;
}
setTrainingStatus('validating');
addLog('Validando disponibilidad de datos...', 'info');
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],
});
try {
// Start training job
addLog('Iniciando trabajo de entrenamiento ML...', 'info');
const response = await trainingService.createTrainingJob({
start_date: undefined,
end_date: undefined
});
const job = response.data;
setCurrentJob(job);
setTrainingStatus('training');
addLog(`Trabajo de entrenamiento iniciado: ${job.id}`, 'success');
// Initialize WebSocket connection for real-time updates
const ws = new WebSocketService(tenantId, job.id);
wsRef.current = ws;
// Set up WebSocket event listeners
ws.subscribe('progress', (message: TrainingProgressMessage) => {
console.log('Training progress received:', message);
setProgress(message.progress.percentage);
setCurrentStep(message.progress.current_step);
setEstimatedTimeRemaining(message.progress.estimated_time_remaining);
addLog(
`${message.progress.current_step} - ${message.progress.products_completed}/${message.progress.products_total} productos procesados (${message.progress.percentage}%)`,
'info'
);
});
ws.subscribe('completed', (message: TrainingCompletedMessage) => {
console.log('Training completed:', message);
setTrainingStatus('completed');
setProgress(100);
const metrics: TrainingMetrics = {
accuracy: message.results.performance_metrics.accuracy,
mape: message.results.performance_metrics.mape,
mae: message.results.performance_metrics.mae,
rmse: message.results.performance_metrics.rmse
};
setMetrics(metrics);
addLog('¡Entrenamiento ML completado exitosamente!', 'success');
addLog(`${message.results.successful_trainings} modelos creados exitosamente`, 'success');
addLog(`Duración total: ${Math.round(message.results.training_duration / 60)} minutos`, 'info');
createAlert({
type: 'success',
category: 'system',
priority: 'medium',
title: 'Entrenamiento completado',
message: `Tu modelo de IA ha sido entrenado exitosamente. Precisión: ${(metrics.accuracy * 100).toFixed(1)}%`,
source: 'onboarding'
});
// Update parent data
onDataChange({
...data,
trainingStatus: 'completed',
trainingProgress: 100,
trainingJob: { ...job, status: 'completed', progress: 100, metrics },
trainingLogs,
trainingMetrics: metrics
});
// Disconnect WebSocket
ws.disconnect();
wsRef.current = null;
});
ws.subscribe('error', (message: TrainingErrorMessage) => {
console.error('Training error received:', message);
setTrainingStatus('failed');
addLog(`Error en entrenamiento: ${message.error}`, 'error');
createAlert({
type: 'error',
category: 'system',
priority: 'high',
title: 'Error en entrenamiento',
message: message.error,
source: 'onboarding'
});
// Disconnect WebSocket
ws.disconnect();
wsRef.current = null;
});
// Connect to WebSocket
await ws.connect();
addLog('Conectado a WebSocket para actualizaciones en tiempo real', 'info');
} catch (error) {
console.error('Training start error:', error);
setTrainingStatus('failed');
const errorMessage = error instanceof Error ? error.message : 'Error al iniciar entrenamiento';
addLog(`Error: ${errorMessage}`, 'error');
createAlert({
type: 'error',
category: 'system',
priority: 'high',
title: 'Error al iniciar entrenamiento',
message: errorMessage,
source: 'onboarding'
});
// Clean up WebSocket if it was created
if (wsRef.current) {
wsRef.current.disconnect();
wsRef.current = null;
}
if (!success) {
console.error('Error starting training');
setHasStarted(false);
}
};
// Cleanup WebSocket on unmount
@@ -324,18 +187,18 @@ export const MLTrainingStep: React.FC<OnboardingStepProps> = ({
const validation = validateDataRequirements();
console.log('MLTrainingStep - useEffect validation:', validation);
if (validation.isValid && trainingStatus === 'idle' && data.autoStartTraining) {
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(() => {
startTraining();
handleStartTraining();
}, 1000);
return () => clearTimeout(timer);
}
}, [data.allStepData, data.autoStartTraining, trainingStatus]);
}, [allStepData, data.autoStartTraining, status]);
const getStatusIcon = () => {
switch (trainingStatus) {
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" />;
@@ -346,7 +209,7 @@ export const MLTrainingStep: React.FC<OnboardingStepProps> = ({
};
const getStatusColor = () => {
switch (trainingStatus) {
switch (status) {
case 'completed': return 'text-[var(--color-success)]';
case 'failed': return 'text-[var(--color-error)]';
case 'training':
@@ -356,7 +219,7 @@ export const MLTrainingStep: React.FC<OnboardingStepProps> = ({
};
const getStatusMessage = () => {
switch (trainingStatus) {
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...';
@@ -489,7 +352,7 @@ export const MLTrainingStep: React.FC<OnboardingStepProps> = ({
</Card>
{/* Training Metrics */}
{metrics && trainingStatus === 'completed' && (
{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" />
@@ -525,10 +388,10 @@ export const MLTrainingStep: React.FC<OnboardingStepProps> = ({
)}
{/* Manual Start Button (if not auto-started) */}
{trainingStatus === 'idle' && (
{status === 'idle' && (
<Card className="p-6 text-center">
<Button
onClick={startTraining}
onClick={handleStartTraining}
className="bg-[var(--color-primary)] hover:bg-[var(--color-primary)]/90"
size="lg"
>