import React, { useState, useMemo } from 'react'; import { Brain, TrendingUp, AlertCircle, Play, RotateCcw, Eye, Loader, CheckCircle } from 'lucide-react'; import { Button, Badge, Modal, Table, Select, StatsGrid, StatusCard, SearchAndFilter, type FilterConfig, Card, EmptyState } from '../../../../components/ui'; import { PageHeader } from '../../../../components/layout'; import { useToast } from '../../../../hooks/ui/useToast'; import { useCurrentTenant } from '../../../../stores/tenant.store'; import { useIngredients } from '../../../../api/hooks/inventory'; import { useModels, useActiveModel, useTrainSingleProduct, useModelMetrics, useModelPerformance, useTenantTrainingStatistics } from '../../../../api/hooks/training'; import { ModelDetailsModal, RetrainModelModal } from '../../../../components/domain/forecasting'; import type { IngredientResponse } from '../../../../api/types/inventory'; import type { TrainedModelResponse, SingleProductTrainingRequest } from '../../../../api/types/training'; // Actual API response structure (different from expected paginated response) interface ModelsApiResponse { tenant_id: string; models: TrainedModelResponse[]; total_returned: number; active_only: boolean; pagination: any; enhanced_features: boolean; repository_integration: boolean; } interface ModelStatus { ingredient: IngredientResponse; hasModel: boolean; model?: TrainedModelResponse; isTraining: boolean; trainingJobId?: string; lastTrainingDate?: string; accuracy?: number; status: 'no_model' | 'active' | 'training' | 'error'; } const ModelsConfigPage: React.FC = () => { const { addToast } = useToast(); const currentTenant = useCurrentTenant(); const tenantId = currentTenant?.id || ''; const [selectedIngredient, setSelectedIngredient] = useState(null); const [selectedModel, setSelectedModel] = useState(null); const [showTrainingModal, setShowTrainingModal] = useState(false); const [showRetrainModal, setShowRetrainModal] = useState(false); const [showModelDetailsModal, setShowModelDetailsModal] = useState(false); const [trainingSettings, setTrainingSettings] = useState>({ seasonality_mode: 'additive', daily_seasonality: true, weekly_seasonality: true, yearly_seasonality: false, }); // API hooks const { data: ingredients = [], isLoading: ingredientsLoading } = useIngredients(tenantId); const { data: modelsData, isLoading: modelsLoading, error: modelsError } = useModels(tenantId); const { data: statistics, error: statsError } = useTenantTrainingStatistics(tenantId); const trainMutation = useTrainSingleProduct(); // Debug: Log the models data structure React.useEffect(() => { console.log('Models data structure:', { modelsData, modelsLoading, modelsError, ingredients: ingredients.length }); }, [modelsData, modelsLoading, modelsError, ingredients]); // Build model status for each ingredient const modelStatuses = useMemo(() => { // Handle different possible data structures from the API response let models: TrainedModelResponse[] = []; // The API actually returns { models: [...], tenant_id: ..., total_returned: ... } const apiResponse = modelsData as any as ModelsApiResponse; if (apiResponse?.models && Array.isArray(apiResponse.models)) { models = apiResponse.models; } else if (modelsData?.data && Array.isArray(modelsData.data)) { models = modelsData.data; } else if (modelsData && Array.isArray(modelsData)) { models = modelsData as any; } console.log('Processing models:', { modelsData, extractedModels: models, modelsCount: models.length, ingredientsCount: ingredients.length }); return ingredients.map((ingredient: IngredientResponse) => { const model = models.find((m: any) => { // Focus only on ID-based matching return m.inventory_product_id === ingredient.id || String(m.inventory_product_id) === String(ingredient.id); }); const isTraining = false; // We'll track this separately for active training jobs console.log(`Ingredient ${ingredient.name} (${ingredient.id}):`, { hasModel: !!model, model: model ? { id: model.model_id, created: model.created_at, inventory_product_id: model.inventory_product_id } : null }); return { ingredient, hasModel: !!model, model, isTraining, lastTrainingDate: model?.created_at || undefined, accuracy: model ? (model.training_metrics?.mape !== undefined ? (100 - model.training_metrics.mape) : (model as any).mape !== undefined ? (100 - (model as any).mape) : undefined) : undefined, status: model ? (isTraining ? 'training' : 'active') : 'no_model' }; }); }, [ingredients, modelsData]); // Calculate orphaned models (models for ingredients that no longer exist) const orphanedModels = useMemo(() => { const apiResponse = modelsData as any as ModelsApiResponse; const models = apiResponse?.models || []; const ingredientIds = new Set(ingredients.map(ing => ing.id)); return models.filter((model: any) => !ingredientIds.has(model.inventory_product_id)); }, [modelsData, ingredients]); // Filter and search const [searchTerm, setSearchTerm] = useState(''); const [statusFilter, setStatusFilter] = useState('all'); const filteredStatuses = useMemo(() => { return modelStatuses.filter(status => { const matchesSearch = status.ingredient.name.toLowerCase().includes(searchTerm.toLowerCase()); const matchesStatus = statusFilter === 'all' || status.status === statusFilter; return matchesSearch && matchesStatus; }); }, [modelStatuses, searchTerm, statusFilter]); const handleTrainModel = async () => { if (!selectedIngredient) return; try { await trainMutation.mutateAsync({ tenantId, inventoryProductId: selectedIngredient.id, request: trainingSettings }); addToast(`Entrenamiento iniciado para ${selectedIngredient.name}`, { type: 'success' }); setShowTrainingModal(false); } catch (error) { addToast('Error al iniciar el entrenamiento', { type: 'error' }); } }; const handleViewModelDetails = async (ingredient: IngredientResponse) => { setSelectedIngredient(ingredient); // Find the model for this ingredient const model = modelStatuses.find(status => status.ingredient.id === ingredient.id)?.model; if (model) { setSelectedModel(model); } setShowModelDetailsModal(true); }; const handleStartTraining = (ingredient: IngredientResponse) => { setSelectedIngredient(ingredient); setShowTrainingModal(true); }; const handleStartRetraining = (ingredient: IngredientResponse) => { setSelectedIngredient(ingredient); // Find and set the model for this ingredient const model = modelStatuses.find(status => status.ingredient.id === ingredient.id)?.model; if (model) { setSelectedModel(model); } setShowRetrainModal(true); }; const handleRetrain = async (settings: SingleProductTrainingRequest) => { if (!selectedIngredient) return; try { await trainMutation.mutateAsync({ tenantId, inventoryProductId: selectedIngredient.id, request: settings }); addToast(`Reentrenamiento iniciado para ${selectedIngredient.name}`, { type: 'success' }); setShowRetrainModal(false); setSelectedIngredient(null); setSelectedModel(null); } catch (error) { addToast('Error al reentrenar el modelo', { type: 'error' }); } }; if (ingredientsLoading || modelsLoading) { return (
{ingredientsLoading ? 'Cargando ingredientes...' : 'Cargando modelos...'}
); } if (modelsError) { console.error('Error loading models:', modelsError); } return (
{/* Statistics Cards */} s.hasModel).length, icon: Brain, variant: 'default', }, { title: 'Sin Modelo', value: modelStatuses.filter(s => s.status === 'no_model').length, icon: AlertCircle, variant: 'warning', }, { title: 'Modelos Activos', value: modelStatuses.filter(s => s.status === 'active').length, icon: CheckCircle, variant: 'success', }, { title: 'Precisión Promedio', value: statsError ? 'N/A' : (statistics?.models?.average_accuracy !== undefined && statistics?.models?.average_accuracy !== null ? `${Number(statistics.models.average_accuracy).toFixed(1)}%` : 'N/A'), icon: TrendingUp, variant: 'success', }, { title: 'Total Modelos', value: modelStatuses.length, icon: Brain, variant: 'info', }, { title: 'Modelos Huérfanos', value: orphanedModels.length, icon: AlertCircle, variant: 'error', }, ]} columns={3} /> {/* Search and Filter Controls */} setStatusFilter(value as string), placeholder: 'Todos los estados', options: [ { value: 'no_model', label: 'Sin modelo' }, { value: 'active', label: 'Activo' }, { value: 'training', label: 'Entrenando' }, { value: 'error', label: 'Error' } ] } ] as FilterConfig[]} /> {/* Models Grid */} {filteredStatuses.length === 0 ? ( ) : (
{( filteredStatuses.map((status) => { // Get status configuration for the StatusCard const statusConfig = { color: status.status === 'active' ? '#10B981' // green for active : status.status === 'no_model' ? '#6B7280' // gray for no model : status.status === 'training' ? '#F59E0B' // amber for training : '#EF4444', // red for error text: status.status === 'active' ? 'Activo' : status.status === 'no_model' ? 'Sin Modelo' : status.status === 'training' ? 'Entrenando' : 'Error', icon: status.status === 'active' ? CheckCircle : status.status === 'no_model' ? AlertCircle : status.status === 'training' ? Loader : AlertCircle, isCritical: status.status === 'error', isHighlight: status.status === 'training' }; return ( { if (status.hasModel) { handleViewModelDetails(status.ingredient); } else { handleStartTraining(status.ingredient); } }} actions={[ // Primary action - View details or train model { label: status.hasModel ? 'Ver Detalles' : 'Entrenar', icon: status.hasModel ? Eye : Play, onClick: () => status.hasModel ? handleViewModelDetails(status.ingredient) : handleStartTraining(status.ingredient), priority: 'primary' as const }, // Secondary action - Retrain if model exists ...(status.hasModel ? [{ label: 'Reentrenar', icon: RotateCcw, onClick: () => handleStartRetraining(status.ingredient), priority: 'secondary' as const }] : []) ]} /> ); }) )}
)} {/* Training Modal */} setShowTrainingModal(false)} title={`Entrenar Modelo - ${selectedIngredient?.name}`} size="lg" >

Configuración de Entrenamiento

Configure los parámetros para el entrenamiento del modelo de predicción de demanda.

setTrainingSettings(prev => ({ ...prev, daily_seasonality: e.target.checked }))} className="rounded border-[var(--border-primary)]" /> Estacionalidad diaria
{/* Model Details Modal */} {selectedModel && ( setShowModelDetailsModal(false)} model={selectedModel} /> )} {/* Retrain Model Modal */} {selectedIngredient && ( { setShowRetrainModal(false); setSelectedIngredient(null); setSelectedModel(null); }} ingredient={selectedIngredient} currentModel={selectedModel} onRetrain={handleRetrain} isLoading={trainMutation.isPending} /> )}
); }; export default ModelsConfigPage;