Files
bakery-ia/frontend/src/components/domain/forecasting/ModelDetailsModal.tsx
2025-09-20 23:30:54 +02:00

404 lines
16 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React from 'react';
import { StatusModal, StatusModalSection } from '@/components/ui/StatusModal/StatusModal';
import { Badge } from '@/components/ui/Badge';
import { Tooltip } from '@/components/ui/Tooltip';
import { TrainedModelResponse, TrainingMetrics } from '@/types/training';
import { Activity, TrendingUp, Calendar, Settings, Lightbulb, AlertTriangle, CheckCircle, Target } from 'lucide-react';
interface ModelDetailsModalProps {
isOpen: boolean;
onClose: () => void;
model: TrainedModelResponse;
}
// Helper function to determine performance color based on accuracy
const getPerformanceColor = (accuracy: number): string => {
if (accuracy >= 90) return 'var(--color-success)';
if (accuracy >= 80) return 'var(--color-info)';
if (accuracy >= 70) return 'var(--color-warning)';
if (accuracy >= 60) return 'var(--color-warning-dark)';
return 'var(--color-error)';
};
// Helper function to determine performance message for bakery owners
const getPerformanceMessage = (accuracy: number): string => {
if (accuracy >= 90) return 'Excelente: Las predicciones son muy precisas, ideales para planificar producción';
if (accuracy >= 80) return 'Bueno: Las predicciones son confiables para planificación diaria';
if (accuracy >= 70) return 'Aceptable: Predicciones útiles pero considera monitorear de cerca';
if (accuracy >= 60) return 'Necesita mejora: Predicciones menos precisas, considera reentrenar';
return 'Poco confiable: El modelo necesita ser reentrenado con más datos';
};
// Helper function to format date
const formatDate = (dateString: string): string => {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
};
// Helper function to calculate accuracy from MAPE
const calculateAccuracy = (mape: number | undefined): number => {
if (mape === undefined || mape === null || isNaN(mape)) return 0;
// MAPE in the API response is already in percentage form (e.g., 8.5 = 8.5%)
const accuracy = Math.max(0, Math.min(100, 100 - mape));
return accuracy;
};
// Feature tag component
const FeatureTag: React.FC<{ feature: string }> = ({ feature }) => {
const getIcon = (feature: string) => {
const featureLower = feature.toLowerCase();
if (featureLower.includes('temperature')) return '☀️';
if (featureLower.includes('weekend')) return '📅';
if (featureLower.includes('holiday')) return '🎉';
if (featureLower.includes('precipitation')) return '🌧️';
if (featureLower.includes('traffic')) return '🚗';
return '📊';
};
const getTooltip = (feature: string) => {
const featureLower = feature.toLowerCase();
if (featureLower.includes('temperature')) return 'El clima afecta lo que los clientes quieren comprar - días calurosos significan menos productos horneados calientes';
if (featureLower.includes('weekend')) return 'Tus patrones de ventas son diferentes los fines de semana vs días de semana';
if (featureLower.includes('holiday')) return 'Días especiales y festivos cambian cuánto compra la gente';
if (featureLower.includes('precipitation')) return 'La lluvia o nieve afecta cuántos clientes visitan tu panadería';
if (featureLower.includes('traffic')) return 'Calles transitadas usualmente significan más clientes';
return `${feature}: Esto ayuda a predecir cuánto venderás`;
};
return (
<Tooltip content={getTooltip(feature)}>
<Badge
variant="secondary"
className="flex items-center gap-1 text-xs md:text-sm px-2 py-1"
role="button"
tabIndex={0}
aria-label={`Característica: ${feature}. ${getTooltip(feature)}`}
>
<span className="text-xs" aria-hidden="true">{getIcon(feature)}</span>
<span className="truncate max-w-[120px]">{feature}</span>
</Badge>
</Tooltip>
);
};
const ModelDetailsModal: React.FC<ModelDetailsModalProps> = ({
isOpen,
onClose,
model
}) => {
// Early return if model is not provided
if (!model) {
return (
<StatusModal
isOpen={isOpen}
onClose={onClose}
mode="view"
title="Model Details"
sections={[]}
actions={[]}
size="xl"
/>
);
}
// Calculate business metrics from technical metrics
// The API response has metrics directly on the model object, not nested in training_metrics
const accuracy = calculateAccuracy((model as any).mape || model.training_metrics?.mape);
const performanceColor = getPerformanceColor(accuracy);
const performanceMessage = getPerformanceMessage(accuracy);
// Prepare sections for StatusModal
const sections: StatusModalSection[] = [
{
title: "Resumen",
icon: Activity,
fields: [
{
label: "Rendimiento del Modelo",
value: (
<div className="w-full">
<div className="flex justify-between text-sm mb-1">
<span className="font-medium">Precisión</span>
<span className="font-bold" style={{ color: performanceColor }} aria-label={`Precisión del modelo: ${accuracy.toFixed(0)} porciento`}>
{accuracy.toFixed(0)}%
</span>
</div>
<div className="w-full bg-[var(--bg-secondary)] rounded-full h-2.5" role="progressbar" aria-valuenow={accuracy} aria-valuemin={0} aria-valuemax={100} aria-label="Progreso de rendimiento del modelo">
<div
className="h-2.5 rounded-full transition-all duration-300 ease-out"
style={{
width: `${accuracy}%`,
backgroundColor: performanceColor
}}
></div>
</div>
<div className="text-xs text-[var(--text-secondary)] mt-2">
{performanceMessage}
</div>
</div>
),
span: 2
},
{
label: "Estado",
value: (
<span
className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium"
style={{
backgroundColor: (model as any).is_active
? 'color-mix(in srgb, var(--color-success) 20%, transparent)'
: 'color-mix(in srgb, var(--color-error) 20%, transparent)',
color: (model as any).is_active ? 'var(--color-success)' : 'var(--color-error)'
}}
aria-label={`Estado del modelo: ${(model as any).is_active ? 'Activo' : 'Inactivo'}`}
>
<span
className="inline-block w-2 h-2 rounded-full mr-2"
style={{
backgroundColor: (model as any).is_active ? 'var(--color-success)' : 'var(--color-error)'
}}
></span>
{(model as any).is_active ? 'Activo' : 'Inactivo'}
</span>
)
},
{
label: "Última Actualización",
value: formatDate(model.created_at || new Date().toISOString())
},
{
label: "Período de Entrenamiento",
value: `${formatDate((model as any).training_start_date || model.training_period?.start_date || new Date().toISOString())} - ${formatDate((model as any).training_end_date || model.training_period?.end_date || new Date().toISOString())}`
}
]
},
{
title: "Métricas de Rendimiento",
icon: TrendingUp,
fields: [
{
label: "Tasa de Precisión",
value: `${accuracy.toFixed(0)}%`,
highlight: true
},
{
label: "Diferencia Típica",
value: (() => {
const maeValue = ((model as any).mae || model.training_metrics?.mae || 0).toFixed(0);
return (
<div className="space-y-1">
<div className="font-medium">±{maeValue} productos</div>
<div className="text-xs text-[var(--text-secondary)]">
En promedio, las predicciones se desvían por {maeValue} productos
</div>
</div>
);
})()
},
{
label: "Confiabilidad General",
value: `${(((model as any).r2_score || model.training_metrics?.r2_score || 0) * 100).toFixed(0)}% confiable`
}
]
},
{
title: "Qué Significa Esto para Tu Panadería",
icon: Target,
fields: [
{
label: "Precisión de las Predicciones",
value: (() => {
const mapeValue = Math.round((model as any).mape || model.training_metrics?.mape || 0);
return (
<div className="space-y-2">
<div className="text-sm font-medium text-[var(--text-primary)]">
Error promedio: {mapeValue}%
</div>
<div className="text-xs text-[var(--text-secondary)] leading-relaxed">
<strong>Ejemplo práctico:</strong> Si el modelo predice que venderás 100 panes,
es muy probable que vendas entre {100 - mapeValue} y {100 + mapeValue} panes.
Mientras menor sea este porcentaje, más precisas son las predicciones.
</div>
</div>
);
})(),
span: 2
},
{
label: "Potencial de Reducción de Desperdicio",
value: accuracy >= 80
? `Podría ayudar a reducir el desperdicio diario en ~${Math.round(accuracy / 8)}% mediante mejor planificación`
: accuracy >= 60
? 'Alguna reducción de desperdicio posible con monitoreo cuidadoso'
: 'Precisión del modelo demasiado baja para reducción confiable de desperdicio',
highlight: true
}
]
},
{
title: "Factores que Considera el Modelo",
icon: Settings,
fields: [
{
label: "Información que Analiza",
value: (() => {
const features = ((model as any).features_used || model.features_used || []);
const featureCount = features.length;
if (featureCount === 0) {
return (
<div className="text-sm text-[var(--text-secondary)] italic">
Usando datos básicos de ventas e historial
</div>
);
}
return (
<div className="space-y-3">
<div className="text-sm text-[var(--text-secondary)]">
El modelo analiza <strong>{featureCount} factores diferentes</strong> para hacer predicciones más precisas.
</div>
<div className="grid grid-cols-2 gap-3 text-xs">
<div className="space-y-1">
<div className="font-medium text-[var(--text-primary)]">📅 Temporales:</div>
<div className="text-[var(--text-secondary)]">Días de semana, festivos, estaciones</div>
</div>
<div className="space-y-1">
<div className="font-medium text-[var(--text-primary)]">🌤 Clima:</div>
<div className="text-[var(--text-secondary)]">Temperatura, lluvia, humedad</div>
</div>
<div className="space-y-1">
<div className="font-medium text-[var(--text-primary)]">🚶 Tráfico:</div>
<div className="text-[var(--text-secondary)]">Peatones, congestión, velocidad</div>
</div>
<div className="space-y-1">
<div className="font-medium text-[var(--text-primary)]">📊 Ventas:</div>
<div className="text-[var(--text-secondary)]">Historial, patrones, tendencias</div>
</div>
</div>
<div className="text-xs text-[var(--text-secondary)] bg-[var(--bg-secondary)] rounded-md p-3 border-l-4 border-[var(--color-info)]">
<strong>💡 En resumen:</strong> Cuantos más factores analiza el modelo, más precisas son las predicciones para tu panadería.
</div>
</div>
);
})(),
span: 2
}
]
},
{
title: "Detalles Técnicos",
icon: Calendar,
fields: [
{
label: "Método de Predicción",
value: model.model_type || 'Pronóstico con IA'
},
{
label: "Última Actualización",
value: formatDate(model.created_at || new Date().toISOString())
},
{
label: "Período de Entrenamiento",
value: `${formatDate((model as any).training_start_date || model.training_period?.start_date || new Date().toISOString())} a ${formatDate((model as any).training_end_date || model.training_period?.end_date || new Date().toISOString())}`,
span: 2
}
]
},
{
title: "Perspectivas de Negocio",
icon: Lightbulb,
fields: [
{
label: "Qué funciona mejor",
value: (
<div className="flex items-start">
<CheckCircle className="text-[var(--color-success)] mr-2 mt-0.5 flex-shrink-0" size={16} />
<span>Este modelo te da las predicciones más precisas para días de semana regulares</span>
</div>
),
span: 2
},
{
label: "Ten cuidado con",
value: (
<div className="flex items-start">
<AlertTriangle className="text-[var(--color-warning)] mr-2 mt-0.5 flex-shrink-0" size={16} />
<span>Las predicciones pueden ser menos confiables durante festivos y eventos especiales</span>
</div>
),
span: 2
},
{
label: "Patrones descubiertos",
value: ((model as any).features_used || model.features_used || []).some((f: string) => f.toLowerCase().includes('weekend'))
? "Tu negocio muestra patrones diferentes entre días de semana y fines de semana"
: "Este modelo ha aprendido tus patrones regulares de ventas",
span: 2
},
{
label: "Próximos pasos",
value: accuracy < 80
? "Considera reentrenar con datos más recientes para mejorar la precisión"
: "Tu modelo está funcionando bien. Continúa monitoreando las predicciones",
span: 2
}
]
}
];
// Actions for the modal - removed console.log placeholders for production readiness
const actions = [
{
label: 'Actualizar Modelo',
variant: 'primary' as const,
onClick: () => {
// TODO: Implement model retraining functionality
// This should trigger a new training job for the product
}
},
{
label: 'Ver Predicciones',
variant: 'secondary' as const,
onClick: () => {
// TODO: Navigate to forecast history or predictions view
// This should show historical predictions vs actual sales
}
}
];
// Only show deactivate if model is active
if ((model as any).is_active) {
actions.push({
label: 'Desactivar',
variant: 'secondary' as const,
onClick: () => {
// TODO: Implement model deactivation
// This should set model status to inactive
}
});
}
return (
<StatusModal
isOpen={isOpen}
onClose={onClose}
mode="view"
title="Detalles de Predicción de Ventas"
subtitle={`Para ${model.model_type || 'tu producto'} • Actualizado ${model.created_at ? formatDate(model.created_at) : 'recientemente'}`}
sections={sections}
actions={actions}
size="xl"
actionsPosition="footer"
/>
);
};
export default ModelDetailsModal;