Add AI insights feature
This commit is contained in:
@@ -16,10 +16,11 @@ const AIInsightsPage: React.FC = () => {
|
||||
const { t } = useTranslation('reasoning');
|
||||
|
||||
// Fetch real insights from API
|
||||
// Note: Backend expects status values: 'new', 'acknowledged', 'in_progress', 'applied', 'dismissed', 'expired'
|
||||
// We fetch 'new' and 'acknowledged' insights (not dismissed, applied, or expired)
|
||||
const { data: insightsData, isLoading, refetch } = useAIInsights(
|
||||
tenantId || '',
|
||||
{
|
||||
status: 'active',
|
||||
category: selectedCategory === 'all' ? undefined : selectedCategory,
|
||||
limit: 100,
|
||||
},
|
||||
@@ -44,20 +45,20 @@ const AIInsightsPage: React.FC = () => {
|
||||
|
||||
const categories = [
|
||||
{ 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 },
|
||||
{ value: 'production', label: 'Producción', count: stats?.by_category?.production || 0 },
|
||||
{ value: 'sales', label: 'Ventas', count: stats?.by_category?.sales || 0 },
|
||||
{ value: 'demand', label: 'Pronósticos', count: stats?.by_category?.demand || stats?.by_category?.forecasting || 0 },
|
||||
{ value: 'inventory', label: 'Inventario', count: stats?.by_category?.inventory || 0 },
|
||||
{ value: 'procurement', label: 'Compras', count: stats?.by_category?.procurement || 0 },
|
||||
];
|
||||
|
||||
const aiMetrics = {
|
||||
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,
|
||||
averageConfidence: stats?.average_confidence ? Math.round(stats.average_confidence) : 0,
|
||||
highPriorityInsights: (stats?.high_priority_count || 0) + (stats?.critical_priority_count || 0),
|
||||
mediumPriorityInsights: stats?.medium_priority_count || 0,
|
||||
lowPriorityInsights: stats?.low_priority_count || 0,
|
||||
};
|
||||
|
||||
const getTypeIcon = (type: string) => {
|
||||
@@ -258,22 +259,73 @@ const AIInsightsPage: React.FC = () => {
|
||||
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">{insight.title}</h3>
|
||||
<p className="text-[var(--text-secondary)] mb-3">{getInsightDescription(insight)}</p>
|
||||
<p className="text-sm font-medium text-[var(--color-success)] mb-4">{insight.impact}</p>
|
||||
|
||||
{/* Metrics */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
||||
{Object.entries(insight.metrics).map(([key, value]) => (
|
||||
<div key={key} className="bg-[var(--bg-secondary)] p-3 rounded-lg">
|
||||
<p className="text-xs text-[var(--text-tertiary)] uppercase tracking-wider">
|
||||
{key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}
|
||||
</p>
|
||||
<p className="text-sm font-semibold text-[var(--text-primary)]">{value}</p>
|
||||
{/* Impact */}
|
||||
{insight.impact_value && insight.impact_type && (
|
||||
<div className="mb-4 p-3 bg-[var(--color-success)]/10 border border-[var(--color-success)]/20 rounded-lg">
|
||||
<p className="text-sm font-medium text-[var(--color-success)]">
|
||||
💰 Impacto: {insight.impact_type.replace(/_/g, ' ')} - {insight.impact_value} {insight.impact_unit || ''}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Recommendation */}
|
||||
{insight.recommendation_actions && insight.recommendation_actions.length > 0 && (
|
||||
<div className="mb-4 p-4 bg-[var(--color-primary)]/5 border border-[var(--color-primary)]/20 rounded-lg">
|
||||
<div className="flex items-start gap-2 mb-2">
|
||||
<Lightbulb className="w-5 h-5 text-[var(--color-primary)] flex-shrink-0 mt-0.5" />
|
||||
<h4 className="text-sm font-semibold text-[var(--text-primary)]">Recomendaciones</h4>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<ul className="space-y-2 ml-7">
|
||||
{insight.recommendation_actions.map((action, idx) => (
|
||||
<li key={idx} className="text-sm text-[var(--text-secondary)] flex items-start gap-2">
|
||||
<span className="text-[var(--color-primary)] flex-shrink-0">•</span>
|
||||
<span>{action.label || action.action}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Metrics - Only show non-redundant metrics */}
|
||||
{insight.metrics_json && Object.keys(insight.metrics_json).length > 0 && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
||||
{Object.entries(insight.metrics_json)
|
||||
.filter(([key]) => !['pattern', 'recommendation'].includes(key)) // Filter out already displayed data
|
||||
.map(([key, value]) => {
|
||||
// Format the value for display
|
||||
let displayValue: string;
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
// For objects, try to display them nicely
|
||||
if (Object.keys(value).length < 5) {
|
||||
displayValue = Object.entries(value)
|
||||
.map(([k, v]) => `${k}: ${v}`)
|
||||
.join(', ');
|
||||
} else {
|
||||
displayValue = JSON.stringify(value, null, 2);
|
||||
}
|
||||
} else if (typeof value === 'number') {
|
||||
displayValue = value.toLocaleString();
|
||||
} else {
|
||||
displayValue = String(value);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={key} className="bg-[var(--bg-secondary)] p-3 rounded-lg">
|
||||
<p className="text-xs text-[var(--text-tertiary)] uppercase tracking-wider">
|
||||
{key.replace(/_/g, ' ').replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())}
|
||||
</p>
|
||||
<p className="text-sm font-semibold text-[var(--text-primary)] break-words">
|
||||
{displayValue}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs text-[var(--text-tertiary)]">{insight.timestamp}</p>
|
||||
<p className="text-xs text-[var(--text-tertiary)]">{insight.created_at ? new Date(insight.created_at).toLocaleString() : ''}</p>
|
||||
{insight.actionable && (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
|
||||
Reference in New Issue
Block a user