import React, { useState, useMemo } from 'react'; import { Brain, TrendingUp, AlertCircle, Play, RotateCcw, Eye, Loader, CheckCircle } from 'lucide-react'; import { Button, Card, Badge, Modal, Table, Select, Input, StatsGrid, StatusCard } 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 } 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 [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, accuracy: model?.training_metrics?.mape ? (100 - model.training_metrics.mape) : 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); }; 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 Huérfanos', value: orphanedModels.length, icon: AlertCircle, variant: 'info', }, { title: 'Precisión Promedio', value: statsError ? 'N/A' : (statistics?.average_accuracy ? `${(100 - statistics.average_accuracy).toFixed(1)}%` : 'N/A'), icon: TrendingUp, variant: 'success', }, ]} columns={4} /> {/* Orphaned Models Warning */} {orphanedModels.length > 0 && (

Modelos Huérfanos Detectados

Se encontraron {orphanedModels.length} modelos entrenados para ingredientes que ya no existen en el inventario. Estos modelos pueden ser eliminados para optimizar el espacio de almacenamiento.

)} {/* Filters */}
setSearchTerm(e.target.value)} />
setTrainingSettings(prev => ({ ...prev, seasonality_mode: value as any }))} options={[ { value: 'additive', label: 'Aditivo' }, { value: 'multiplicative', label: 'Multiplicativo' } ]} />

Patrones Estacionales

{/* Model Details Modal */} {selectedModel && ( setShowModelDetailsModal(false)} model={selectedModel} /> )} ); }; export default ModelsConfigPage;