Add onboarding flow improvements

This commit is contained in:
Urtzi Alfaro
2025-09-03 14:06:38 +02:00
parent 0fb9f9d0f0
commit a55d48e635
31 changed files with 3813 additions and 6251 deletions

View File

@@ -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>
);
};