ca-grow-ops-manager/frontend/src/pages/InsightsDashboard.tsx
fullsizemalt 71e58dd4c7
Some checks are pending
Deploy to Production / deploy (push) Waiting to run
Test / backend-test (push) Waiting to run
Test / frontend-test (push) Waiting to run
feat: Linear-inspired UI redesign with Space Grotesk headlines
- Complete UI refactor with charcoal/bone color palette
- Add Space Grotesk font for headlines, Inter for body
- Update all 24+ pages with new design system
- Add LinearPrimitives reusable components
- Improve dark mode support throughout
- Add subtle micro-animations and transitions
2025-12-12 14:29:47 -08:00

300 lines
13 KiB
TypeScript

import { useState, useEffect } from 'react';
import {
Brain, TrendingUp, AlertTriangle, Zap, Target,
CheckCircle, BarChart3, RefreshCw, Loader2
} from 'lucide-react';
import api from '../lib/api';
import { PageHeader, MetricCard, EmptyState, CardSkeleton } from '../components/ui/LinearPrimitives';
interface Prediction {
batchId: string;
accuracy: string;
predictedAt: string;
}
interface Anomaly {
id: string;
entityType: string;
entityId: string;
anomalyType: string;
severity: string;
description: string;
isResolved: boolean;
detectedAt: string;
}
interface InsightsDashboardData {
predictions: {
count: number;
avgAccuracy: string;
recentAccuracy: Prediction[];
};
anomalies: {
unresolved: number;
recent: Anomaly[];
};
performance: {
topBatches: {
batchId: string;
costPerGram: string;
totalCost: number;
yieldGrams: number;
}[];
};
}
export default function InsightsDashboard() {
const [dashboard, setDashboard] = useState<InsightsDashboardData | null>(null);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
const res = await api.get('/api/insights/dashboard');
setDashboard(res.data);
} catch (error) {
console.error('Failed to load insights:', error);
} finally {
setLoading(false);
}
};
const handleRefresh = async () => {
setRefreshing(true);
await loadData();
setRefreshing(false);
};
const resolveAnomaly = async (id: string) => {
try {
await api.post(`/api/insights/anomalies/${id}/resolve`);
loadData();
} catch (error) {
console.error('Failed to resolve anomaly:', error);
}
};
if (loading) {
return (
<div className="space-y-6 animate-in">
<PageHeader title="AI Insights" subtitle="Loading..." />
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
{Array.from({ length: 3 }).map((_, i) => <CardSkeleton key={i} />)}
</div>
</div>
);
}
const anomalyCount = dashboard?.anomalies.unresolved || 0;
return (
<div className="space-y-6 pb-20 animate-in">
<PageHeader
title="AI Insights"
subtitle="Predictions, anomalies, and analytics"
actions={
<button
onClick={handleRefresh}
disabled={refreshing}
className="btn btn-ghost p-2"
>
<RefreshCw size={16} className={refreshing ? 'animate-spin' : ''} />
</button>
}
/>
{/* Quick Stats */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<MetricCard
icon={Target}
label="Model Accuracy"
value={dashboard?.predictions.avgAccuracy || 'N/A'}
subtitle={`${dashboard?.predictions.count || 0} predictions`}
accent="accent"
/>
<div className={`
card p-4
${anomalyCount > 0 ? 'border-warning/30 bg-warning-muted' : 'border-success/30 bg-success-muted'}
`}>
<div className="flex items-center gap-2 mb-1">
<AlertTriangle className={anomalyCount > 0 ? 'text-warning' : 'text-success'} size={16} />
<span className="text-xs text-tertiary uppercase tracking-wider">Active Anomalies</span>
</div>
<div className={`text-2xl font-semibold ${anomalyCount > 0 ? 'text-warning' : 'text-success'}`}>
{anomalyCount}
</div>
<p className="text-xs text-tertiary mt-1">
{anomalyCount === 0 ? 'All systems normal' : 'Requires attention'}
</p>
</div>
<MetricCard
icon={BarChart3}
label="Top Performers"
value={dashboard?.performance.topBatches.length || 0}
subtitle="Batches with cost data"
accent="accent"
/>
</div>
{/* Anomalies Section */}
<div className="card overflow-hidden">
<div className="p-4 border-b border-subtle">
<h2 className="text-sm font-medium text-primary flex items-center gap-2">
<AlertTriangle className="text-warning" size={16} />
Detected Anomalies
</h2>
</div>
<div className="divide-y divide-subtle">
{dashboard?.anomalies.recent.map(anomaly => (
<div
key={anomaly.id}
className={`p-4 flex items-start gap-3 ${anomaly.isResolved ? 'bg-tertiary' : ''}`}
>
<div className={`
w-8 h-8 rounded-md flex items-center justify-center
${anomaly.isResolved ? 'bg-tertiary text-tertiary' :
anomaly.severity === 'CRITICAL' ? 'bg-destructive-muted text-destructive' :
'bg-warning-muted text-warning'}
`}>
{anomaly.isResolved ? <CheckCircle size={16} /> : <AlertTriangle size={16} />}
</div>
<div className="flex-1">
<div className="flex items-center gap-2">
<span className={`font-medium text-sm ${anomaly.isResolved ? 'text-tertiary' : 'text-primary'}`}>
{anomaly.description}
</span>
<span className={`
text-[10px] font-medium px-1.5 py-0.5 rounded
${anomaly.severity === 'CRITICAL' ? 'badge-destructive' : 'badge-warning'}
`}>
{anomaly.severity}
</span>
</div>
<div className="text-[10px] text-tertiary mt-1">
{anomaly.entityType} · {anomaly.anomalyType.replace('_', ' ')} ·{' '}
{new Date(anomaly.detectedAt).toLocaleString()}
</div>
</div>
{!anomaly.isResolved && (
<button
onClick={() => resolveAnomaly(anomaly.id)}
className="text-xs text-accent hover:underline"
>
Resolve
</button>
)}
</div>
))}
{(!dashboard?.anomalies.recent || dashboard.anomalies.recent.length === 0) && (
<div className="p-8 text-center">
<CheckCircle className="mx-auto text-success mb-2" size={24} />
<p className="text-tertiary text-sm">No anomalies detected</p>
</div>
)}
</div>
</div>
{/* Top Performing Batches */}
<div className="card overflow-hidden">
<div className="p-4 border-b border-subtle">
<h2 className="text-sm font-medium text-primary flex items-center gap-2">
<TrendingUp className="text-success" size={16} />
Best Cost Efficiency
</h2>
</div>
<div className="p-4">
<table className="w-full">
<thead>
<tr className="text-left text-[10px] text-tertiary uppercase tracking-wider">
<th className="pb-3 font-medium">Batch</th>
<th className="pb-3 font-medium">Cost/Gram</th>
<th className="pb-3 font-medium">Total Cost</th>
<th className="pb-3 font-medium">Yield</th>
</tr>
</thead>
<tbody className="text-sm">
{dashboard?.performance.topBatches.map((batch, idx) => (
<tr key={batch.batchId} className="border-t border-subtle">
<td className="py-3">
<div className="flex items-center gap-2">
<span className={`
w-5 h-5 rounded-full flex items-center justify-center text-[10px] font-semibold
${idx === 0 ? 'bg-warning-muted text-warning' :
idx === 1 ? 'bg-tertiary text-secondary' :
idx === 2 ? 'bg-accent-muted text-accent' :
'bg-tertiary text-tertiary'}
`}>
{idx + 1}
</span>
<span className="font-medium text-primary">
{batch.batchId.substring(0, 8)}...
</span>
</div>
</td>
<td className="py-3 font-semibold text-success">${batch.costPerGram}/g</td>
<td className="py-3 text-secondary">${batch.totalCost.toLocaleString()}</td>
<td className="py-3 text-secondary">{batch.yieldGrams?.toLocaleString() || 'N/A'}g</td>
</tr>
))}
{(!dashboard?.performance.topBatches || dashboard.performance.topBatches.length === 0) && (
<tr>
<td colSpan={4} className="py-8 text-center text-tertiary text-sm">
No batch performance data available
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
{/* Recent Predictions */}
<div className="card overflow-hidden">
<div className="p-4 border-b border-subtle">
<h2 className="text-sm font-medium text-primary flex items-center gap-2">
<Zap className="text-accent" size={16} />
Recent Predictions
</h2>
</div>
<div className="p-4 space-y-2">
{dashboard?.predictions.recentAccuracy.map(pred => (
<div
key={pred.batchId + pred.predictedAt}
className="flex items-center justify-between p-3 bg-tertiary rounded-md"
>
<div>
<span className="font-medium text-primary text-sm">
Batch {pred.batchId.substring(0, 8)}
</span>
<p className="text-[10px] text-tertiary">
{new Date(pred.predictedAt).toLocaleString()}
</p>
</div>
<div className={`
text-base font-semibold
${parseFloat(pred.accuracy) >= 80 ? 'text-success' :
parseFloat(pred.accuracy) >= 60 ? 'text-warning' : 'text-destructive'}
`}>
{pred.accuracy}
</div>
</div>
))}
{(!dashboard?.predictions.recentAccuracy || dashboard.predictions.recentAccuracy.length === 0) && (
<p className="text-center text-tertiary py-4 text-sm">
No predictions with verified accuracy yet
</p>
)}
</div>
</div>
</div>
);
}