Simplify the onboardinf flow components
This commit is contained in:
@@ -1,422 +1,264 @@
|
||||
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';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Button } from '../../../ui/Button';
|
||||
import { useCurrentTenant } from '../../../../stores/tenant.store';
|
||||
import { useCreateTrainingJob, useTrainingWebSocket } from '../../../../api/hooks/training';
|
||||
|
||||
// Type definitions for training messages (will be moved to API types later)
|
||||
interface TrainingProgressMessage {
|
||||
type: 'training_progress';
|
||||
progress: number;
|
||||
interface MLTrainingStepProps {
|
||||
onNext: () => void;
|
||||
onPrevious: () => void;
|
||||
onComplete: (data?: any) => void;
|
||||
isFirstStep: boolean;
|
||||
isLastStep: boolean;
|
||||
}
|
||||
|
||||
interface TrainingProgress {
|
||||
stage: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface TrainingCompletedMessage {
|
||||
type: 'training_completed';
|
||||
metrics: TrainingMetrics;
|
||||
}
|
||||
|
||||
interface TrainingErrorMessage {
|
||||
type: 'training_error';
|
||||
error: string;
|
||||
}
|
||||
|
||||
interface TrainingMetrics {
|
||||
accuracy: number;
|
||||
mape: number;
|
||||
mae: number;
|
||||
rmse: number;
|
||||
}
|
||||
|
||||
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;
|
||||
message: string;
|
||||
currentStep?: string;
|
||||
estimatedTimeRemaining?: number;
|
||||
}
|
||||
|
||||
// Using the proper training service from services/api/training.service.ts
|
||||
|
||||
export const MLTrainingStep: React.FC<OnboardingStepProps> = ({
|
||||
data,
|
||||
onDataChange,
|
||||
onNext,
|
||||
export const MLTrainingStep: React.FC<MLTrainingStepProps> = ({
|
||||
onPrevious,
|
||||
isFirstStep,
|
||||
isLastStep
|
||||
onComplete,
|
||||
isFirstStep
|
||||
}) => {
|
||||
const user = useAuthUser();
|
||||
const [trainingProgress, setTrainingProgress] = useState<TrainingProgress | null>(null);
|
||||
const [isTraining, setIsTraining] = useState(false);
|
||||
const [error, setError] = useState<string>('');
|
||||
const [jobId, setJobId] = useState<string | null>(null);
|
||||
|
||||
const currentTenant = useCurrentTenant();
|
||||
|
||||
// Use the onboarding hooks
|
||||
const {
|
||||
startTraining,
|
||||
trainingOrchestration: {
|
||||
status,
|
||||
progress,
|
||||
currentStep,
|
||||
estimatedTimeRemaining,
|
||||
job,
|
||||
logs,
|
||||
metrics
|
||||
},
|
||||
data: allStepData,
|
||||
isLoading,
|
||||
error,
|
||||
clearError
|
||||
} = useOnboarding();
|
||||
|
||||
// Local state for UI-only elements
|
||||
const [hasStarted, setHasStarted] = useState(false);
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const createTrainingJob = useCreateTrainingJob();
|
||||
|
||||
// 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 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');
|
||||
// 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...'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sales data must be imported for ML training to work
|
||||
if (!hasImportResults) {
|
||||
missingItems.push('Datos de ventas importados');
|
||||
}
|
||||
|
||||
// Check if products were approved in review step
|
||||
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 = allStepData?.inventoryConfigured &&
|
||||
allStepData?.inventoryItems &&
|
||||
allStepData.inventoryItems.length > 0;
|
||||
|
||||
if (!hasInventoryConfig) {
|
||||
missingItems.push('Inventario configurado');
|
||||
}
|
||||
|
||||
// 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)');
|
||||
}
|
||||
|
||||
console.log('MLTrainingStep - Validation result:', {
|
||||
isValid: missingItems.length === 0,
|
||||
missingItems,
|
||||
hasProcessingResults,
|
||||
hasImportResults,
|
||||
hasApprovedProducts,
|
||||
hasInventoryConfig
|
||||
});
|
||||
|
||||
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);
|
||||
if (!currentTenant?.id) {
|
||||
setError('No se encontró información del tenant');
|
||||
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],
|
||||
setIsTraining(true);
|
||||
setError('');
|
||||
setTrainingProgress({
|
||||
stage: 'preparing',
|
||||
progress: 0,
|
||||
message: 'Preparando datos para entrenamiento...'
|
||||
});
|
||||
|
||||
if (!success) {
|
||||
console.error('Error starting training');
|
||||
setHasStarted(false);
|
||||
}
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
// 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);
|
||||
const formatTime = (seconds?: number) => {
|
||||
if (!seconds) return '';
|
||||
|
||||
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)]" />;
|
||||
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`;
|
||||
}
|
||||
};
|
||||
|
||||
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)]';
|
||||
}
|
||||
};
|
||||
|
||||
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';
|
||||
}
|
||||
};
|
||||
|
||||
// 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>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Header */}
|
||||
<div className="space-y-6">
|
||||
<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()}
|
||||
</div>
|
||||
<h2 className={`text-3xl font-bold mb-4 ${getStatusColor()}`}>
|
||||
Entrenamiento de IA
|
||||
</h2>
|
||||
<p className={`text-lg mb-2 ${getStatusColor()}`}>
|
||||
{getStatusMessage()}
|
||||
</p>
|
||||
<p className="text-[var(--text-secondary)]">
|
||||
Creando tu asistente inteligente personalizado con tus datos de ventas e inventario
|
||||
<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.
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
|
||||
{/* 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()}
|
||||
</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>
|
||||
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 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)}%
|
||||
</p>
|
||||
<p className="text-xs text-[var(--text-secondary)]">Precisión</p>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
{/* 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>
|
||||
|
||||
{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>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
)}
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="flex justify-between pt-6 border-t border-[var(--border-primary)]">
|
||||
<Button
|
||||
variant="outline"
|
||||
{/* Actions */}
|
||||
<div className="flex justify-between">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onPrevious}
|
||||
disabled={isFirstStep}
|
||||
disabled={isFirstStep || isTraining}
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onNext}
|
||||
disabled={trainingStatus !== 'completed'}
|
||||
className="bg-[var(--color-primary)] hover:bg-[var(--color-primary)]/90"
|
||||
>
|
||||
{isLastStep ? 'Finalizar' : 'Siguiente'}
|
||||
</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>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user