Improve AI logic
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user