Add onboarding flow improvements
This commit is contained in:
@@ -0,0 +1,780 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Brain, Activity, Zap, CheckCircle, AlertCircle, TrendingUp } from 'lucide-react';
|
||||
import { Button, Card, Badge } from '../../../ui';
|
||||
import { OnboardingStepProps } from '../OnboardingWizard';
|
||||
|
||||
interface TrainingMetrics {
|
||||
accuracy: number;
|
||||
precision: number;
|
||||
recall: number;
|
||||
f1_score: number;
|
||||
training_loss: number;
|
||||
validation_loss: number;
|
||||
epochs_completed: number;
|
||||
total_epochs: number;
|
||||
}
|
||||
|
||||
interface TrainingLog {
|
||||
timestamp: string;
|
||||
message: string;
|
||||
level: 'info' | 'warning' | 'error' | 'success';
|
||||
}
|
||||
|
||||
// Enhanced Mock ML training service that matches backend behavior
|
||||
const mockMLService = {
|
||||
startTraining: async (trainingData: any) => {
|
||||
return new Promise((resolve) => {
|
||||
let progress = 0;
|
||||
let step_count = 0;
|
||||
const total_steps = 12;
|
||||
|
||||
// Backend-matching training steps and messages
|
||||
const trainingSteps = [
|
||||
{ step: 'data_validation', message: 'Validando datos de entrenamiento...', progress: 10 },
|
||||
{ step: 'data_preparation_start', message: 'Preparando conjunto de datos...', progress: 20 },
|
||||
{ step: 'feature_engineering', message: 'Creando características para el modelo...', progress: 30 },
|
||||
{ step: 'data_preparation_complete', message: 'Preparación de datos completada', progress: 35 },
|
||||
{ step: 'ml_training_start', message: 'Iniciando entrenamiento del modelo Prophet...', progress: 40 },
|
||||
{ step: 'model_fitting', message: 'Ajustando modelo a los datos históricos...', progress: 55 },
|
||||
{ step: 'pattern_detection', message: 'Detectando patrones estacionales y tendencias...', progress: 70 },
|
||||
{ step: 'validation', message: 'Validando precisión del modelo...', progress: 80 },
|
||||
{ step: 'training_complete', message: 'Entrenamiento ML completado', progress: 85 },
|
||||
{ step: 'storing_models', message: 'Guardando modelo entrenado...', progress: 90 },
|
||||
{ step: 'performance_metrics', message: 'Calculando métricas de rendimiento...', progress: 95 },
|
||||
{ step: 'completed', message: 'Tu Asistente Inteligente está listo!', progress: 100 }
|
||||
];
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (step_count >= trainingSteps.length) {
|
||||
clearInterval(interval);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentStep = trainingSteps[step_count];
|
||||
progress = currentStep.progress;
|
||||
|
||||
// Generate realistic metrics that improve over time
|
||||
const stepProgress = step_count / trainingSteps.length;
|
||||
const baseAccuracy = 0.65;
|
||||
const maxAccuracy = 0.93;
|
||||
const currentAccuracy = baseAccuracy + (maxAccuracy - baseAccuracy) * Math.pow(stepProgress, 0.8);
|
||||
|
||||
const metrics: TrainingMetrics = {
|
||||
accuracy: Math.min(maxAccuracy, currentAccuracy + (Math.random() * 0.02 - 0.01)),
|
||||
precision: Math.min(0.95, 0.70 + stepProgress * 0.23 + (Math.random() * 0.02 - 0.01)),
|
||||
recall: Math.min(0.94, 0.68 + stepProgress * 0.24 + (Math.random() * 0.02 - 0.01)),
|
||||
f1_score: Math.min(0.94, 0.69 + stepProgress * 0.23 + (Math.random() * 0.02 - 0.01)),
|
||||
training_loss: Math.max(0.08, 1.2 - stepProgress * 1.0 + (Math.random() * 0.05 - 0.025)),
|
||||
validation_loss: Math.max(0.10, 1.3 - stepProgress * 1.1 + (Math.random() * 0.06 - 0.03)),
|
||||
epochs_completed: Math.floor(stepProgress * 15) + 1,
|
||||
total_epochs: 15
|
||||
};
|
||||
|
||||
// Generate step-specific logs that match backend behavior
|
||||
const logs: TrainingLog[] = [];
|
||||
|
||||
// Add the current step message
|
||||
logs.push({
|
||||
timestamp: new Date().toLocaleTimeString(),
|
||||
message: currentStep.message,
|
||||
level: currentStep.step === 'completed' ? 'success' : 'info'
|
||||
});
|
||||
|
||||
// Add specific logs based on the training step
|
||||
if (currentStep.step === 'data_validation') {
|
||||
logs.push({
|
||||
timestamp: new Date().toLocaleTimeString(),
|
||||
message: `Productos analizados: ${trainingData.products?.length || 3}`,
|
||||
level: 'info'
|
||||
});
|
||||
logs.push({
|
||||
timestamp: new Date().toLocaleTimeString(),
|
||||
message: `Registros de inventario: ${trainingData.inventory?.length || 3}`,
|
||||
level: 'info'
|
||||
});
|
||||
} else if (currentStep.step === 'ml_training_start') {
|
||||
logs.push({
|
||||
timestamp: new Date().toLocaleTimeString(),
|
||||
message: 'Usando modelo Prophet optimizado para panadería',
|
||||
level: 'info'
|
||||
});
|
||||
} else if (currentStep.step === 'model_fitting') {
|
||||
logs.push({
|
||||
timestamp: new Date().toLocaleTimeString(),
|
||||
message: `Precisión actual: ${(metrics.accuracy * 100).toFixed(1)}%`,
|
||||
level: 'info'
|
||||
});
|
||||
} else if (currentStep.step === 'pattern_detection') {
|
||||
logs.push({
|
||||
timestamp: new Date().toLocaleTimeString(),
|
||||
message: 'Patrones estacionales detectados: días de la semana, horas pico',
|
||||
level: 'success'
|
||||
});
|
||||
} else if (currentStep.step === 'validation') {
|
||||
logs.push({
|
||||
timestamp: new Date().toLocaleTimeString(),
|
||||
message: `MAE: ${(Math.random() * 2 + 1.5).toFixed(2)}, MAPE: ${((1 - metrics.accuracy) * 100).toFixed(1)}%`,
|
||||
level: 'info'
|
||||
});
|
||||
} else if (currentStep.step === 'storing_models') {
|
||||
logs.push({
|
||||
timestamp: new Date().toLocaleTimeString(),
|
||||
message: `Modelo guardado: bakery_prophet_${Date.now()}`,
|
||||
level: 'success'
|
||||
});
|
||||
}
|
||||
|
||||
// Emit progress event that matches backend WebSocket message structure
|
||||
window.dispatchEvent(new CustomEvent('mlTrainingProgress', {
|
||||
detail: {
|
||||
type: 'progress',
|
||||
job_id: `training_${Date.now()}`,
|
||||
data: {
|
||||
progress,
|
||||
current_step: currentStep.step,
|
||||
step_details: currentStep.message,
|
||||
products_completed: Math.floor(step_count / 3),
|
||||
products_total: Math.max(3, trainingData.products?.length || 3),
|
||||
estimated_time_remaining_minutes: Math.max(0, Math.floor((total_steps - step_count) * 0.75))
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
// Keep old format for backward compatibility
|
||||
status: progress >= 100 ? 'completed' : 'training',
|
||||
currentPhase: currentStep.message,
|
||||
metrics,
|
||||
logs
|
||||
}
|
||||
}));
|
||||
|
||||
step_count++;
|
||||
|
||||
if (progress >= 100) {
|
||||
clearInterval(interval);
|
||||
|
||||
// Generate final results matching backend response structure
|
||||
const finalAccuracy = 0.89 + Math.random() * 0.05;
|
||||
const finalResult = {
|
||||
success: true,
|
||||
job_id: `training_${Date.now()}`,
|
||||
tenant_id: 'demo_tenant',
|
||||
status: 'completed',
|
||||
training_results: {
|
||||
total_products: Math.max(3, trainingData.products?.length || 3),
|
||||
successful_trainings: Math.max(3, trainingData.products?.length || 3),
|
||||
failed_trainings: 0,
|
||||
products: Array.from({length: Math.max(3, trainingData.products?.length || 3)}, (_, i) => ({
|
||||
inventory_product_id: `product_${i + 1}`,
|
||||
status: 'completed',
|
||||
model_id: `model_${i + 1}_${Date.now()}`,
|
||||
data_points: 45 + Math.floor(Math.random() * 30),
|
||||
metrics: {
|
||||
mape: (1 - finalAccuracy) * 100 + (Math.random() * 2 - 1),
|
||||
mae: 1.5 + Math.random() * 1.0,
|
||||
rmse: 2.1 + Math.random() * 1.2,
|
||||
r2_score: finalAccuracy + (Math.random() * 0.02 - 0.01)
|
||||
}
|
||||
})),
|
||||
overall_training_time_seconds: 45 + Math.random() * 15
|
||||
},
|
||||
data_summary: {
|
||||
sales_records: trainingData.inventory?.length * 30 || 1500,
|
||||
weather_records: 90,
|
||||
traffic_records: 85,
|
||||
date_range: {
|
||||
start: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
end: new Date().toISOString()
|
||||
},
|
||||
data_sources_used: ['bakery_sales', 'weather_forecast', 'madrid_traffic'],
|
||||
constraints_applied: {}
|
||||
},
|
||||
finalMetrics: {
|
||||
accuracy: finalAccuracy,
|
||||
precision: finalAccuracy * 0.98,
|
||||
recall: finalAccuracy * 0.96,
|
||||
f1_score: finalAccuracy * 0.97,
|
||||
training_loss: 0.12 + Math.random() * 0.05,
|
||||
validation_loss: 0.15 + Math.random() * 0.05,
|
||||
total_epochs: 15
|
||||
},
|
||||
modelId: `bakery_prophet_${Date.now()}`,
|
||||
deploymentUrl: '/api/v1/training/models/predict',
|
||||
trainingDuration: `${(45 + Math.random() * 15).toFixed(1)} segundos`,
|
||||
datasetSize: `${trainingData.inventory?.length * 30 || 1500} registros`,
|
||||
modelVersion: '2.1.0',
|
||||
completed_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
resolve(finalResult);
|
||||
}
|
||||
}, 1800); // Matches backend timing
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getTrainingPhase = (epoch: number, total: number) => {
|
||||
const progress = epoch / total;
|
||||
if (progress <= 0.3) return 'Inicializando modelo de IA...';
|
||||
if (progress <= 0.6) return 'Entrenando patrones de venta...';
|
||||
if (progress <= 0.8) return 'Optimizando predicciones...';
|
||||
if (progress <= 0.95) return 'Validando rendimiento...';
|
||||
return 'Finalizando entrenamiento...';
|
||||
};
|
||||
|
||||
export const MLTrainingStep: React.FC<OnboardingStepProps> = ({
|
||||
data,
|
||||
onDataChange,
|
||||
onNext,
|
||||
onPrevious,
|
||||
isFirstStep,
|
||||
isLastStep
|
||||
}) => {
|
||||
const [trainingStatus, setTrainingStatus] = useState(data.trainingStatus || 'pending');
|
||||
const [progress, setProgress] = useState(data.trainingProgress || 0);
|
||||
const [currentPhase, setCurrentPhase] = useState(data.currentPhase || '');
|
||||
const [metrics, setMetrics] = useState<TrainingMetrics | null>(data.trainingMetrics || null);
|
||||
const [logs, setLogs] = useState<TrainingLog[]>(data.trainingLogs || []);
|
||||
const [finalResults, setFinalResults] = useState<any>(data.finalResults || null);
|
||||
|
||||
// New state variables for backend-compatible data
|
||||
const [productsCompleted, setProductsCompleted] = useState(0);
|
||||
const [productsTotal, setProductsTotal] = useState(0);
|
||||
const [estimatedTimeRemaining, setEstimatedTimeRemaining] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Listen for ML training progress updates
|
||||
const handleProgress = (event: CustomEvent) => {
|
||||
const detail = event.detail;
|
||||
|
||||
// Handle new backend-compatible WebSocket message format
|
||||
if (detail.type === 'progress' && detail.data) {
|
||||
const progressData = detail.data;
|
||||
setProgress(progressData.progress || 0);
|
||||
setCurrentPhase(progressData.step_details || progressData.current_step || 'En progreso...');
|
||||
|
||||
// Update products progress if available
|
||||
if (progressData.products_completed !== undefined) {
|
||||
setProductsCompleted(progressData.products_completed);
|
||||
}
|
||||
if (progressData.products_total !== undefined) {
|
||||
setProductsTotal(progressData.products_total);
|
||||
}
|
||||
|
||||
// Update time estimate if available
|
||||
if (progressData.estimated_time_remaining_minutes !== undefined) {
|
||||
setEstimatedTimeRemaining(progressData.estimated_time_remaining_minutes);
|
||||
}
|
||||
|
||||
// Handle metrics (from legacy format for backward compatibility)
|
||||
if (detail.metrics) {
|
||||
setMetrics(detail.metrics);
|
||||
}
|
||||
|
||||
// Handle logs (from legacy format for backward compatibility)
|
||||
if (detail.logs && detail.logs.length > 0) {
|
||||
setLogs(prev => [...prev, ...detail.logs]);
|
||||
}
|
||||
|
||||
// Check completion status
|
||||
if (progressData.progress >= 100) {
|
||||
setTrainingStatus('completed');
|
||||
} else {
|
||||
setTrainingStatus('training');
|
||||
}
|
||||
} else {
|
||||
// Handle legacy format for backward compatibility
|
||||
const { progress: newProgress, metrics: newMetrics, logs: newLogs, status, currentPhase: newPhase } = detail;
|
||||
|
||||
setProgress(newProgress || 0);
|
||||
setMetrics(newMetrics);
|
||||
setCurrentPhase(newPhase || 'En progreso...');
|
||||
setTrainingStatus(status || 'training');
|
||||
|
||||
if (newLogs && newLogs.length > 0) {
|
||||
setLogs(prev => [...prev, ...newLogs]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('mlTrainingProgress', handleProgress as EventListener);
|
||||
return () => window.removeEventListener('mlTrainingProgress', handleProgress as EventListener);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Update parent data when state changes
|
||||
onDataChange({
|
||||
...data,
|
||||
trainingStatus,
|
||||
trainingProgress: progress,
|
||||
currentPhase,
|
||||
trainingMetrics: metrics,
|
||||
trainingLogs: logs,
|
||||
finalResults
|
||||
});
|
||||
}, [trainingStatus, progress, currentPhase, metrics, logs, finalResults]);
|
||||
|
||||
// Auto-start training when coming from suppliers step
|
||||
useEffect(() => {
|
||||
if (data.autoStartTraining && trainingStatus === 'pending') {
|
||||
const timer = setTimeout(() => {
|
||||
startTraining();
|
||||
// Remove the auto-start flag so it doesn't trigger again
|
||||
onDataChange({ ...data, autoStartTraining: false });
|
||||
}, 1000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [data.autoStartTraining, trainingStatus]);
|
||||
|
||||
const startTraining = async () => {
|
||||
// Access data from previous steps through allStepData
|
||||
const inventoryData = data.allStepData?.inventory?.inventoryItems ||
|
||||
data.allStepData?.['inventory-setup']?.inventoryItems ||
|
||||
data.inventoryItems || [];
|
||||
|
||||
const detectedProducts = data.allStepData?.review?.detectedProducts?.filter((p: any) => p.status === 'approved') ||
|
||||
data.allStepData?.['data-processing']?.detectedProducts?.filter((p: any) => p.status === 'approved') ||
|
||||
data.detectedProducts?.filter((p: any) => p.status === 'approved') || [];
|
||||
|
||||
const salesValidation = data.allStepData?.['data-processing']?.validation ||
|
||||
data.allStepData?.review?.validation ||
|
||||
data.validation || {};
|
||||
|
||||
// If no data is available, create mock data for demo purposes
|
||||
let finalInventoryData = inventoryData;
|
||||
let finalDetectedProducts = detectedProducts;
|
||||
let finalValidation = salesValidation;
|
||||
|
||||
if (inventoryData.length === 0 && detectedProducts.length === 0) {
|
||||
console.log('No data found from previous steps, using mock data for demo');
|
||||
|
||||
// Create mock data for demonstration
|
||||
finalInventoryData = [
|
||||
{ id: '1', name: 'Harina de Trigo', category: 'ingredient', current_stock: 50, min_stock: 20, max_stock: 100, unit: 'kg' },
|
||||
{ id: '2', name: 'Levadura Fresca', category: 'ingredient', current_stock: 5, min_stock: 2, max_stock: 10, unit: 'kg' },
|
||||
{ id: '3', name: 'Pan Integral', category: 'finished_product', current_stock: 20, min_stock: 10, max_stock: 50, unit: 'unidades' }
|
||||
];
|
||||
|
||||
finalDetectedProducts = [
|
||||
{ name: 'Pan Francés', status: 'approved', category: 'Panadería' },
|
||||
{ name: 'Croissants', status: 'approved', category: 'Repostería' },
|
||||
{ name: 'Pan Integral', status: 'approved', category: 'Panadería' }
|
||||
];
|
||||
|
||||
finalValidation = {
|
||||
total_records: 1500,
|
||||
summary: {
|
||||
date_range: '2024-01-01 to 2024-12-31'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
setTrainingStatus('training');
|
||||
setProgress(0);
|
||||
setLogs([{
|
||||
timestamp: new Date().toLocaleTimeString(),
|
||||
message: 'Iniciando entrenamiento del modelo de Machine Learning...',
|
||||
level: 'info'
|
||||
}]);
|
||||
|
||||
try {
|
||||
const result = await mockMLService.startTraining({
|
||||
products: finalDetectedProducts,
|
||||
inventory: finalInventoryData,
|
||||
salesData: finalValidation
|
||||
});
|
||||
|
||||
setFinalResults(result);
|
||||
setTrainingStatus('completed');
|
||||
|
||||
} catch (error) {
|
||||
console.error('ML Training error:', error);
|
||||
setTrainingStatus('error');
|
||||
setLogs(prev => [...prev, {
|
||||
timestamp: new Date().toLocaleTimeString(),
|
||||
message: 'Error durante el entrenamiento del modelo',
|
||||
level: 'error'
|
||||
}]);
|
||||
}
|
||||
};
|
||||
|
||||
const retryTraining = () => {
|
||||
setTrainingStatus('pending');
|
||||
setProgress(0);
|
||||
setMetrics(null);
|
||||
setLogs([]);
|
||||
setFinalResults(null);
|
||||
};
|
||||
|
||||
const getLogIcon = (level: string) => {
|
||||
switch (level) {
|
||||
case 'success': return '✅';
|
||||
case 'warning': return '⚠️';
|
||||
case 'error': return '❌';
|
||||
default: return 'ℹ️';
|
||||
}
|
||||
};
|
||||
|
||||
const getLogColor = (level: string) => {
|
||||
switch (level) {
|
||||
case 'success': return 'text-[var(--color-success)]';
|
||||
case 'warning': return 'text-[var(--color-warning)]';
|
||||
case 'error': return 'text-[var(--color-error)]';
|
||||
default: return 'text-[var(--text-secondary)]';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-6">
|
||||
<div className="w-16 h-16 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<Brain className="w-8 h-8 text-[var(--color-primary)]" />
|
||||
</div>
|
||||
<p className="text-[var(--text-secondary)]">
|
||||
Tu asistente inteligente analizará tus datos históricos para ayudarte a tomar mejores decisiones de negocio
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Auto-start notification */}
|
||||
{data.autoStartTraining && trainingStatus === 'pending' && (
|
||||
<Card className="p-4 bg-[var(--color-info)]/10 border-[var(--color-info)]/20 mb-4">
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<div className="animate-spin w-5 h-5 border-2 border-[var(--color-info)] border-t-transparent rounded-full"></div>
|
||||
<p className="text-[var(--color-info)] font-medium">
|
||||
Creando tu asistente inteligente automáticamente...
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Training Controls */}
|
||||
{trainingStatus === 'pending' && !data.autoStartTraining && (
|
||||
<Card className="p-6 text-center">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">
|
||||
Crear tu Asistente Inteligente
|
||||
</h3>
|
||||
<p className="text-[var(--text-secondary)] mb-6">
|
||||
Analizaremos tus datos de ventas y productos para crear un asistente que te ayude a predecir demanda y optimizar tu inventario.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<Activity className="w-6 h-6 text-[var(--color-info)] mx-auto mb-2" />
|
||||
<h4 className="font-medium text-[var(--text-primary)]">Predicción de Ventas</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)]">Anticipa cuánto vas a vender</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<TrendingUp className="w-6 h-6 text-[var(--color-success)] mx-auto mb-2" />
|
||||
<h4 className="font-medium text-[var(--text-primary)]">Recomendaciones</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)]">Cuánto producir y comprar</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<Zap className="w-6 h-6 text-[var(--color-warning)] mx-auto mb-2" />
|
||||
<h4 className="font-medium text-[var(--text-primary)]">Alertas Automáticas</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)]">Te avisa cuando reordenar</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[var(--color-info)]/10 border border-[var(--color-info)]/20 rounded-lg p-4 mb-6">
|
||||
<h4 className="font-medium text-[var(--color-info)] mb-2">Información que analizaremos:</h4>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm text-[var(--color-info)]">
|
||||
<div>
|
||||
<span className="font-medium">Productos:</span>
|
||||
<br />
|
||||
{(() => {
|
||||
const allStepData = data.allStepData || {};
|
||||
const detectedProducts = allStepData.review?.detectedProducts?.filter((p: any) => p.status === 'approved') ||
|
||||
allStepData['data-processing']?.detectedProducts?.filter((p: any) => p.status === 'approved') ||
|
||||
data.detectedProducts?.filter((p: any) => p.status === 'approved') || [];
|
||||
return detectedProducts.length || 3; // fallback to mock data count
|
||||
})()}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Inventario:</span>
|
||||
<br />
|
||||
{(() => {
|
||||
const allStepData = data.allStepData || {};
|
||||
const inventoryData = allStepData.inventory?.inventoryItems ||
|
||||
allStepData['inventory-setup']?.inventoryItems ||
|
||||
allStepData['inventory']?.inventoryItems ||
|
||||
data.inventoryItems || [];
|
||||
return inventoryData.length || 3; // fallback to mock data count
|
||||
})()} elementos
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Registros:</span>
|
||||
<br />
|
||||
{(() => {
|
||||
const allStepData = data.allStepData || {};
|
||||
const validation = allStepData['data-processing']?.validation ||
|
||||
allStepData.review?.validation ||
|
||||
data.validation || {};
|
||||
return validation.total_records || 1500; // fallback to mock data
|
||||
})()}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Período:</span>
|
||||
<br />
|
||||
{(() => {
|
||||
const allStepData = data.allStepData || {};
|
||||
const validation = allStepData['data-processing']?.validation ||
|
||||
allStepData.review?.validation ||
|
||||
data.validation || {};
|
||||
const dateRange = validation.summary?.date_range || '2024-01-01 to 2024-12-31';
|
||||
return dateRange.split(' to ').map((date: string) =>
|
||||
new Date(date).toLocaleDateString()
|
||||
).join(' - ');
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button onClick={startTraining} className="px-8 py-2">
|
||||
<Brain className="w-4 h-4 mr-2" />
|
||||
Crear mi Asistente Inteligente
|
||||
</Button>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Training Progress */}
|
||||
{trainingStatus === 'training' && (
|
||||
<div className="space-y-6">
|
||||
<Card className="p-6">
|
||||
<div className="text-center mb-6">
|
||||
<div className="animate-pulse w-12 h-12 bg-[var(--color-primary)] rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<Brain className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">
|
||||
Creando tu Asistente Inteligente...
|
||||
</h3>
|
||||
<p className="text-[var(--text-secondary)]">{currentPhase}</p>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="mb-6">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm font-medium text-[var(--text-primary)]">
|
||||
Progreso de Creación
|
||||
</span>
|
||||
<div className="text-right">
|
||||
<span className="text-sm text-[var(--text-secondary)]">
|
||||
{progress.toFixed(0)}%
|
||||
</span>
|
||||
{productsTotal > 0 && (
|
||||
<div className="text-xs text-[var(--text-tertiary)]">
|
||||
{productsCompleted}/{productsTotal} productos
|
||||
</div>
|
||||
)}
|
||||
{estimatedTimeRemaining !== null && estimatedTimeRemaining > 0 && (
|
||||
<div className="text-xs text-[var(--text-tertiary)]">
|
||||
~{estimatedTimeRemaining} min restantes
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full bg-[var(--bg-secondary)] rounded-full h-4">
|
||||
<div
|
||||
className="bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-info)] h-4 rounded-full transition-all duration-1000 relative overflow-hidden"
|
||||
style={{ width: `${progress}%` }}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Current Metrics */}
|
||||
{metrics && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||
<div className="text-center p-3 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<p className="text-xl 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-xl font-bold text-[var(--color-info)]">
|
||||
{metrics.training_loss.toFixed(3)}
|
||||
</p>
|
||||
<p className="text-xs text-[var(--text-secondary)]">Loss</p>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<p className="text-xl font-bold text-[var(--color-primary)]">
|
||||
{metrics.epochs_completed}/{metrics.total_epochs}
|
||||
</p>
|
||||
<p className="text-xs text-[var(--text-secondary)]">Epochs</p>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<p className="text-xl font-bold text-[var(--color-secondary)]">
|
||||
{(metrics.f1_score * 100).toFixed(1)}%
|
||||
</p>
|
||||
<p className="text-xs text-[var(--text-secondary)]">F1-Score</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Training Logs */}
|
||||
<Card className="p-6">
|
||||
<h4 className="font-medium text-[var(--text-primary)] mb-4">Progreso del Asistente</h4>
|
||||
<div className="bg-black rounded-lg p-4 max-h-48 overflow-y-auto font-mono text-sm">
|
||||
{logs.slice(-10).map((log, index) => (
|
||||
<div key={index} className={`mb-1 ${getLogColor(log.level)}`}>
|
||||
<span className="text-[var(--text-quaternary)]">[{log.timestamp}]</span>
|
||||
<span className="ml-2">{getLogIcon(log.level)} {log.message}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Training Completed */}
|
||||
{trainingStatus === 'completed' && finalResults && (
|
||||
<div className="space-y-6">
|
||||
<Card className="p-6">
|
||||
<div className="text-center mb-6">
|
||||
<div className="w-12 h-12 bg-[var(--color-success)] rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<CheckCircle className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-[var(--color-success)] mb-2">
|
||||
¡Tu Asistente Inteligente está Listo!
|
||||
</h3>
|
||||
<p className="text-[var(--text-secondary)]">
|
||||
Tu asistente personalizado está listo para ayudarte con predicciones y recomendaciones
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Final Metrics */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-6">
|
||||
<div className="text-center p-4 bg-[var(--color-success)]/10 rounded-lg">
|
||||
<p className="text-2xl font-bold text-[var(--color-success)]">
|
||||
{(finalResults.finalMetrics.accuracy * 100).toFixed(1)}%
|
||||
</p>
|
||||
<p className="text-xs text-[var(--text-secondary)]">Exactitud</p>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-[var(--color-info)]/10 rounded-lg">
|
||||
<p className="text-2xl font-bold text-[var(--color-info)]">
|
||||
{(finalResults.finalMetrics.precision * 100).toFixed(1)}%
|
||||
</p>
|
||||
<p className="text-xs text-[var(--text-secondary)]">Confiabilidad</p>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-[var(--color-primary)]/10 rounded-lg">
|
||||
<p className="text-2xl font-bold text-[var(--color-primary)]">
|
||||
{(finalResults.finalMetrics.recall * 100).toFixed(1)}%
|
||||
</p>
|
||||
<p className="text-xs text-[var(--text-secondary)]">Cobertura</p>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-[var(--color-secondary-100)] rounded-lg">
|
||||
<p className="text-2xl font-bold text-[var(--color-secondary)]">
|
||||
{(finalResults.finalMetrics.f1_score * 100).toFixed(1)}%
|
||||
</p>
|
||||
<p className="text-xs text-[var(--text-secondary)]">Rendimiento</p>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-[var(--color-warning-100)] rounded-lg">
|
||||
<p className="text-2xl font-bold text-[var(--color-warning)]">
|
||||
{finalResults.finalMetrics.total_epochs}
|
||||
</p>
|
||||
<p className="text-xs text-[var(--text-secondary)]">Iteraciones</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Model Info */}
|
||||
<div className="bg-[var(--bg-secondary)] rounded-lg p-4 mb-6">
|
||||
<h4 className="font-medium text-[var(--text-primary)] mb-3">Información del Modelo:</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="font-medium text-[var(--text-tertiary)]">ID del Modelo:</span>
|
||||
<p className="font-mono text-[var(--text-primary)]">{finalResults.modelId}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-[var(--text-tertiary)]">Versión:</span>
|
||||
<p className="font-mono text-[var(--text-primary)]">{finalResults.modelVersion}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-[var(--text-tertiary)]">Duración:</span>
|
||||
<p className="text-[var(--text-primary)]">{finalResults.trainingDuration}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-[var(--text-tertiary)]">Dataset:</span>
|
||||
<p className="text-[var(--text-primary)]">{finalResults.datasetSize}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-[var(--text-tertiary)]">Endpoint:</span>
|
||||
<p className="font-mono text-[var(--text-primary)]">{finalResults.deploymentUrl}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-[var(--text-tertiary)]">Estado:</span>
|
||||
<p className="text-[var(--color-success)] font-medium">✓ Activo</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Capabilities */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="p-4 border border-[var(--color-success)]/20 rounded-lg">
|
||||
<TrendingUp className="w-6 h-6 text-[var(--color-success)] mb-2" />
|
||||
<h4 className="font-medium text-[var(--text-primary)] mb-1">Predicción de Ventas</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
Te dice cuánto vas a vender cada día
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border border-[var(--color-info)]/20 rounded-lg">
|
||||
<Activity className="w-6 h-6 text-[var(--color-info)] mb-2" />
|
||||
<h4 className="font-medium text-[var(--text-primary)] mb-1">Recomendaciones</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
Te sugiere cuánto producir y comprar
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 border border-[var(--color-warning)]/20 rounded-lg">
|
||||
<Zap className="w-6 h-6 text-[var(--color-warning)] mb-2" />
|
||||
<h4 className="font-medium text-[var(--text-primary)] mb-1">Alertas Automáticas</h4>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
Te avisa cuando necesitas reordenar
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Training Error */}
|
||||
{trainingStatus === 'error' && (
|
||||
<Card className="p-6">
|
||||
<div className="text-center">
|
||||
<div className="w-12 h-12 bg-[var(--color-error)] rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<AlertCircle className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-[var(--color-error)] mb-2">
|
||||
Error al Crear el Asistente
|
||||
</h3>
|
||||
<p className="text-[var(--text-secondary)] mb-4">
|
||||
Ocurrió un problema durante la creación de tu asistente inteligente. Por favor, intenta nuevamente.
|
||||
</p>
|
||||
<Button onClick={retryTraining} variant="outline">
|
||||
Intentar Nuevamente
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Information */}
|
||||
<Card className="p-4 bg-[var(--color-info)]/5 border-[var(--color-info)]/20">
|
||||
<h4 className="font-medium text-[var(--color-info)] mb-2">
|
||||
🎯 ¿Cómo funciona tu Asistente Inteligente?
|
||||
</h4>
|
||||
<ul className="text-sm text-[var(--color-info)] space-y-1">
|
||||
<li>• <strong>Analiza tus ventas:</strong> Estudia tus datos históricos para encontrar patrones</li>
|
||||
<li>• <strong>Entiende tu negocio:</strong> Aprende sobre temporadas altas y bajas</li>
|
||||
<li>• <strong>Hace predicciones:</strong> Te dice cuánto vas a vender cada día</li>
|
||||
<li>• <strong>Da recomendaciones:</strong> Te sugiere cuánto producir y comprar</li>
|
||||
<li>• <strong>Mejora con el tiempo:</strong> Se hace más inteligente con cada venta nueva</li>
|
||||
</ul>
|
||||
</Card>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user