Start integrating the onboarding flow with backend 7
This commit is contained in:
@@ -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"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user