Improve AI logic

This commit is contained in:
Urtzi Alfaro
2025-11-05 13:34:56 +01:00
parent 5c87fbcf48
commit 394ad3aea4
218 changed files with 30627 additions and 7658 deletions

View File

@@ -2,115 +2,60 @@ import React, { useState } from 'react';
import { Brain, TrendingUp, AlertTriangle, Lightbulb, Target, Zap, Download, RefreshCw } from 'lucide-react';
import { Button, Card, Badge } from '../../../../components/ui';
import { AnalyticsPageLayout, AnalyticsCard } from '../../../../components/analytics';
import { useCurrentTenant } from '../../../../stores/tenant.store';
import { useAuthUser } from '../../../../stores/auth.store';
import { useAIInsights, useAIInsightStats, useApplyInsight, useDismissInsight } from '../../../../api/hooks/aiInsights';
import { AIInsight } from '../../../../api/services/aiInsights';
const AIInsightsPage: React.FC = () => {
const [selectedCategory, setSelectedCategory] = useState('all');
const [isRefreshing, setIsRefreshing] = useState(false);
const currentTenant = useCurrentTenant();
const user = useAuthUser();
const tenantId = currentTenant?.id || user?.tenant_id;
const insights = [
// Fetch real insights from API
const { data: insightsData, isLoading, refetch } = useAIInsights(
tenantId || '',
{
id: '1',
type: 'optimization',
priority: 'high',
title: 'Optimización de Producción de Croissants',
description: 'La demanda de croissants aumenta un 23% los viernes. Recomendamos incrementar la producción en 15 unidades.',
impact: 'Aumento estimado de ingresos: €180/semana',
confidence: 87,
category: 'production',
timestamp: '2024-01-26 09:30',
actionable: true,
metrics: {
currentProduction: 45,
recommendedProduction: 60,
expectedIncrease: '+23%'
}
status: 'active',
category: selectedCategory === 'all' ? undefined : selectedCategory,
limit: 100,
},
{
id: '2',
type: 'alert',
priority: 'medium',
title: 'Patrón de Compra en Tardes',
description: 'Los clientes compran más productos salados después de las 16:00. Considera promocionar empanadas durante estas horas.',
impact: 'Potencial aumento de ventas: 12%',
confidence: 92,
category: 'sales',
timestamp: '2024-01-26 08:45',
actionable: true,
metrics: {
afternoonSales: '+15%',
savoryProducts: '68%',
conversionRate: '12.3%'
}
},
{
id: '3',
type: 'prediction',
priority: 'high',
title: 'Predicción de Demanda de San Valentín',
description: 'Se espera un incremento del 40% en la demanda de productos de repostería especiales entre el 10-14 de febrero.',
impact: 'Preparar stock adicional de ingredientes premium',
confidence: 94,
category: 'forecasting',
timestamp: '2024-01-26 07:15',
actionable: true,
metrics: {
expectedIncrease: '+40%',
daysAhead: 18,
recommendedPrep: '3 días'
}
},
{
id: '4',
type: 'recommendation',
priority: 'low',
title: 'Optimización de Inventario de Harina',
description: 'El consumo de harina integral ha disminuido 8% este mes. Considera ajustar las órdenes de compra.',
impact: 'Reducción de desperdicios: €45/mes',
confidence: 78,
category: 'inventory',
timestamp: '2024-01-25 16:20',
actionable: false,
metrics: {
consumption: '-8%',
currentStock: '45kg',
recommendedOrder: '25kg'
}
},
{
id: '5',
type: 'insight',
priority: 'medium',
title: 'Análisis de Satisfacción del Cliente',
description: 'Los clientes valoran más la frescura (95%) que el precio (67%). Enfoque en destacar la calidad artesanal.',
impact: 'Mejorar estrategia de marketing',
confidence: 89,
category: 'customer',
timestamp: '2024-01-25 14:30',
actionable: true,
metrics: {
freshnessScore: '95%',
priceScore: '67%',
qualityScore: '91%'
}
}
];
{ enabled: !!tenantId }
);
// Fetch stats
const { data: stats } = useAIInsightStats(
tenantId || '',
{},
{ enabled: !!tenantId }
);
// Mutations
const applyMutation = useApplyInsight();
const dismissMutation = useDismissInsight();
const insights: AIInsight[] = insightsData?.items || [];
// Use real insights data
const displayInsights = insights;
const categories = [
{ value: 'all', label: 'Todas las Categorías', count: insights.length },
{ value: 'production', label: 'Producción', count: insights.filter(i => i.category === 'production').length },
{ value: 'sales', label: 'Ventas', count: insights.filter(i => i.category === 'sales').length },
{ value: 'forecasting', label: 'Pronósticos', count: insights.filter(i => i.category === 'forecasting').length },
{ value: 'inventory', label: 'Inventario', count: insights.filter(i => i.category === 'inventory').length },
{ value: 'customer', label: 'Clientes', count: insights.filter(i => i.category === 'customer').length },
{ value: 'all', label: 'Todas las Categorías', count: stats?.total_insights || 0 },
{ value: 'production', label: 'Producción', count: stats?.insights_by_category?.production || 0 },
{ value: 'sales', label: 'Ventas', count: stats?.insights_by_category?.sales || 0 },
{ value: 'demand', label: 'Pronósticos', count: stats?.insights_by_category?.demand || 0 },
{ value: 'inventory', label: 'Inventario', count: stats?.insights_by_category?.inventory || 0 },
{ value: 'procurement', label: 'Compras', count: stats?.insights_by_category?.procurement || 0 },
];
const aiMetrics = {
totalInsights: insights.length,
actionableInsights: insights.filter(i => i.actionable).length,
averageConfidence: Math.round(insights.reduce((sum, i) => sum + i.confidence, 0) / insights.length),
highPriorityInsights: insights.filter(i => i.priority === 'high').length,
mediumPriorityInsights: insights.filter(i => i.priority === 'medium').length,
lowPriorityInsights: insights.filter(i => i.priority === 'low').length,
totalInsights: stats?.total_insights || 0,
actionableInsights: stats?.actionable_insights || 0,
averageConfidence: stats?.avg_confidence ? Math.round(stats.avg_confidence) : 0,
highPriorityInsights: stats?.insights_by_priority?.high || stats?.insights_by_priority?.urgent || 0,
mediumPriorityInsights: stats?.insights_by_priority?.medium || 0,
lowPriorityInsights: stats?.insights_by_priority?.low || 0,
};
const getTypeIcon = (type: string) => {
@@ -145,14 +90,32 @@ const AIInsightsPage: React.FC = () => {
}
};
const filteredInsights = selectedCategory === 'all'
? insights
: insights.filter(insight => insight.category === selectedCategory);
const filteredInsights = selectedCategory === 'all'
? displayInsights
: displayInsights.filter(insight => insight.category === selectedCategory);
const handleRefresh = async () => {
setIsRefreshing(true);
await new Promise(resolve => setTimeout(resolve, 2000));
setIsRefreshing(false);
await refetch();
};
const handleApplyInsight = async (insightId: string) => {
if (!tenantId) return;
try {
await applyMutation.mutateAsync({ tenantId, insightId });
await refetch();
} catch (error) {
console.error('Failed to apply insight:', error);
}
};
const handleDismissInsight = async (insightId: string) => {
if (!tenantId) return;
try {
await dismissMutation.mutateAsync({ tenantId, insightId });
await refetch();
} catch (error) {
console.error('Failed to dismiss insight:', error);
}
};
return (
@@ -161,7 +124,7 @@ const AIInsightsPage: React.FC = () => {
description="Insights inteligentes y recomendaciones automáticas para optimizar tu panadería"
subscriptionLoading={false}
hasAccess={true}
dataLoading={isRefreshing}
dataLoading={isLoading || applyMutation.isLoading || dismissMutation.isLoading}
actions={[
{
id: 'refresh',
@@ -169,7 +132,7 @@ const AIInsightsPage: React.FC = () => {
icon: RefreshCw,
onClick: handleRefresh,
variant: 'outline',
disabled: isRefreshing,
disabled: isLoading,
},
{
id: 'export',
@@ -279,9 +242,23 @@ const AIInsightsPage: React.FC = () => {
<div className="flex items-center justify-between">
<p className="text-xs text-[var(--text-tertiary)]">{insight.timestamp}</p>
{insight.actionable && (
<Button size="sm">
Aplicar Recomendación
</Button>
<div className="flex gap-2">
<Button
size="sm"
onClick={() => handleApplyInsight(insight.id)}
disabled={applyMutation.isLoading}
>
Aplicar Recomendación
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleDismissInsight(insight.id)}
disabled={dismissMutation.isLoading}
>
Descartar
</Button>
</div>
)}
</div>
</div>

View File

@@ -370,24 +370,38 @@ const ModelsConfigPage: React.FC = () => {
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
}] : [])
]}
actions={
(() => {
if (status.hasModel) {
// For models that exist: prioritize retraining action as primary (text button)
// and details as secondary (icon button)
return [
{
label: 'Reentrenar',
icon: RotateCcw,
onClick: () => handleStartRetraining(status.ingredient),
priority: 'primary' as const
},
{
label: 'Ver Detalles',
icon: Eye,
onClick: () => handleViewModelDetails(status.ingredient),
priority: 'secondary' as const
}
];
} else {
// For models that don't exist: only train action
return [
{
label: 'Entrenar',
icon: Play,
onClick: () => handleStartTraining(status.ingredient),
priority: 'primary' as const
}
];
}
})()
}
/>
);
})
@@ -479,6 +493,12 @@ const ModelsConfigPage: React.FC = () => {
isOpen={showModelDetailsModal}
onClose={() => setShowModelDetailsModal(false)}
model={selectedModel}
onRetrain={handleRetrain}
onViewPredictions={(modelId) => {
// TODO: Navigate to forecast history or predictions view
// This should show historical predictions vs actual sales
console.log('View predictions for model:', modelId);
}}
/>
)}