forked from 0xWheatyz/SPARC
e0ed39908e
Replace hardcoded dark-theme hex colors in recharts components (tooltips, axes) with a useChartTheme hook that reads the current theme from ThemeContext. Charts now render correctly in both light and dark mode. Closes leeworks-agents/SPARC#1324 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
305 lines
12 KiB
TypeScript
305 lines
12 KiB
TypeScript
import { useState } from 'react';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { analyticsApi } from '../api/client';
|
|
import { AlertCircle, Database } from 'lucide-react';
|
|
import { PieChart, Pie, Cell, BarChart, Bar, LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend } from 'recharts';
|
|
import { useChartTheme } from '../context/useChartTheme';
|
|
|
|
const COLORS = ['#6366f1', '#0ea5e9', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#14b8a6'];
|
|
|
|
export function AnalyticsPage() {
|
|
const [days, setDays] = useState(30);
|
|
const chartTheme = useChartTheme();
|
|
|
|
const { data, isLoading, isError, refetch } = useQuery({
|
|
queryKey: ['analytics', days],
|
|
queryFn: () => analyticsApi.getAnalytics(days),
|
|
});
|
|
|
|
const trendsQuery = useQuery({
|
|
queryKey: ['analytics-trends', days],
|
|
queryFn: () => analyticsApi.getTrends(days),
|
|
});
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h2 className="text-xl font-semibold text-text-primary border-b-2 border-primary/30 pb-2 mb-2">
|
|
Analytics Dashboard
|
|
</h2>
|
|
<p className="text-text-secondary">Loading analytics data...</p>
|
|
</div>
|
|
{/* Skeleton cards */}
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
{[1, 2, 3].map((i) => (
|
|
<div key={i} className="bg-gradient-to-br from-primary/10 to-secondary/10 border border-primary/20 rounded-xl p-5 text-center animate-pulse">
|
|
<div className="h-9 w-16 bg-primary/20 rounded mx-auto mb-2" />
|
|
<div className="h-4 w-24 bg-primary/10 rounded mx-auto" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
{/* Skeleton charts */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
{[1, 2].map((i) => (
|
|
<div key={i} className="bg-bg-card/60 border border-primary/15 rounded-2xl p-6 animate-pulse">
|
|
<div className="h-5 w-40 bg-primary/20 rounded mb-4" />
|
|
<div className="h-[300px] bg-primary/5 rounded" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (isError) {
|
|
return (
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h2 className="text-xl font-semibold text-text-primary border-b-2 border-primary/30 pb-2 mb-2">
|
|
Analytics Dashboard
|
|
</h2>
|
|
</div>
|
|
<div className="bg-gradient-to-br from-primary/10 to-secondary/5 border border-primary/20 rounded-xl p-6">
|
|
<div className="flex items-center gap-3 text-warning mb-2">
|
|
<Database size={24} />
|
|
<span className="font-semibold">Unable to Load Analytics</span>
|
|
</div>
|
|
<p className="text-text-secondary">
|
|
Could not connect to the analytics database. Ensure PostgreSQL is running and
|
|
<code className="bg-bg-card px-2 py-1 rounded mx-1">DATABASE_URL</code> is configured correctly.
|
|
</p>
|
|
<button
|
|
onClick={() => refetch()}
|
|
className="mt-3 text-sm bg-primary/20 hover:bg-primary/30 text-primary font-medium px-4 py-2 rounded-lg transition-colors"
|
|
>
|
|
Retry
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!data || (data.total_messages === 0 && data.by_company.length === 0)) {
|
|
return (
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h2 className="text-xl font-semibold text-text-primary border-b-2 border-primary/30 pb-2 mb-2">
|
|
Analytics Dashboard
|
|
</h2>
|
|
<p className="text-text-secondary">Track historical analysis data and view insights.</p>
|
|
</div>
|
|
<div className="flex items-center gap-2 bg-secondary/10 border border-secondary/20 text-secondary rounded-xl px-4 py-3">
|
|
<AlertCircle size={18} />
|
|
<span>No analytics data available yet. Run some analyses first!</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const companyData = data.by_company.map((c) => ({
|
|
name: (c.company_name || 'Unknown').toUpperCase(),
|
|
value: c.count,
|
|
}));
|
|
|
|
const typeData = data.by_type.map((t) => ({
|
|
name: t.analysis_type || 'Unknown',
|
|
count: t.count,
|
|
}));
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex flex-wrap items-center justify-between gap-4">
|
|
<div>
|
|
<h2 className="text-xl font-semibold text-text-primary border-b-2 border-primary/30 pb-2 mb-2">
|
|
Analytics Dashboard
|
|
</h2>
|
|
<p className="text-text-secondary">Track historical analysis data and view insights.</p>
|
|
</div>
|
|
|
|
{/* Time Range Selector */}
|
|
<select
|
|
value={days}
|
|
onChange={(e) => setDays(Number(e.target.value))}
|
|
className="bg-bg-card/80 border border-primary/30 rounded-xl px-4 py-2 text-text-primary focus:outline-none focus:border-primary"
|
|
>
|
|
<option value={7}>Last 7 days</option>
|
|
<option value={14}>Last 14 days</option>
|
|
<option value={30}>Last 30 days</option>
|
|
<option value={90}>Last 90 days</option>
|
|
</select>
|
|
</div>
|
|
|
|
{/* Summary Metrics */}
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<MetricCard label="Total Analyses" value={data.total_messages} />
|
|
<MetricCard label="Companies Analyzed" value={data.by_company.length} />
|
|
<MetricCard label="Analysis Types" value={data.by_type.length} />
|
|
</div>
|
|
|
|
{/* Charts */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
{/* Pie Chart - Distribution by Company */}
|
|
{companyData.length > 0 && (
|
|
<div className="bg-bg-card/60 border border-primary/15 rounded-2xl p-6">
|
|
<h3 className="text-lg font-semibold text-text-primary mb-4">Distribution by Company</h3>
|
|
<ResponsiveContainer width="100%" height={300}>
|
|
<PieChart>
|
|
<Pie
|
|
data={companyData}
|
|
cx="50%"
|
|
cy="50%"
|
|
innerRadius={60}
|
|
outerRadius={100}
|
|
paddingAngle={2}
|
|
dataKey="value"
|
|
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
|
|
labelLine={false}
|
|
>
|
|
{companyData.map((_, index) => (
|
|
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
|
))}
|
|
</Pie>
|
|
<Tooltip
|
|
contentStyle={chartTheme.tooltipContentStyle}
|
|
/>
|
|
<Legend />
|
|
</PieChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
)}
|
|
|
|
{/* Bar Chart - Analysis Types */}
|
|
{typeData.length > 0 && (
|
|
<div className="bg-bg-card/60 border border-primary/15 rounded-2xl p-6">
|
|
<h3 className="text-lg font-semibold text-text-primary mb-4">Analysis Types</h3>
|
|
<ResponsiveContainer width="100%" height={300}>
|
|
<BarChart data={typeData}>
|
|
<XAxis dataKey="name" stroke={chartTheme.axisStroke} fontSize={12} />
|
|
<YAxis stroke={chartTheme.axisStroke} fontSize={12} />
|
|
<Tooltip
|
|
contentStyle={chartTheme.tooltipContentStyle}
|
|
labelStyle={chartTheme.tooltipLabelStyle}
|
|
/>
|
|
<Bar dataKey="count" fill="#6366f1" radius={[4, 4, 0, 0]} />
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Trend Charts */}
|
|
{trendsQuery.data && (
|
|
<div className="space-y-6">
|
|
<h3 className="text-lg font-semibold text-text-primary border-b-2 border-primary/30 pb-2">
|
|
Trends Over Time
|
|
</h3>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
{/* Patent count over time per company (line chart) */}
|
|
{trendsQuery.data.by_month.length > 0 && (() => {
|
|
// Pivot data: each month as a row, companies as columns
|
|
const companies = [...new Set(trendsQuery.data!.by_month.map(d => d.company_name))];
|
|
const months = [...new Set(trendsQuery.data!.by_month.map(d => d.month))].sort();
|
|
const pivoted = months.map(month => {
|
|
const row: Record<string, string | number> = { month };
|
|
for (const c of companies) {
|
|
const entry = trendsQuery.data!.by_month.find(d => d.month === month && d.company_name === c);
|
|
row[c] = entry?.count || 0;
|
|
}
|
|
return row;
|
|
});
|
|
|
|
return (
|
|
<div className="bg-bg-card/60 border border-primary/15 rounded-2xl p-6">
|
|
<h4 className="text-md font-semibold text-text-primary mb-4">Analyses per Company Over Time</h4>
|
|
<ResponsiveContainer width="100%" height={300}>
|
|
<LineChart data={pivoted}>
|
|
<XAxis dataKey="month" stroke={chartTheme.axisStroke} fontSize={12} />
|
|
<YAxis stroke={chartTheme.axisStroke} fontSize={12} />
|
|
<Tooltip
|
|
contentStyle={chartTheme.tooltipContentStyle}
|
|
labelStyle={chartTheme.tooltipLabelStyle}
|
|
/>
|
|
<Legend />
|
|
{companies.map((company, idx) => (
|
|
<Line
|
|
key={company}
|
|
type="monotone"
|
|
dataKey={company}
|
|
stroke={COLORS[idx % COLORS.length]}
|
|
strokeWidth={2}
|
|
dot={{ r: 4 }}
|
|
name={company.toUpperCase()}
|
|
/>
|
|
))}
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
);
|
|
})()}
|
|
|
|
{/* Analysis type distribution over time (stacked bar) */}
|
|
{trendsQuery.data.by_type_over_time.length > 0 && (() => {
|
|
const types = [...new Set(trendsQuery.data!.by_type_over_time.map(d => d.analysis_type))];
|
|
const months = [...new Set(trendsQuery.data!.by_type_over_time.map(d => d.month))].sort();
|
|
const pivoted = months.map(month => {
|
|
const row: Record<string, string | number> = { month };
|
|
for (const t of types) {
|
|
const entry = trendsQuery.data!.by_type_over_time.find(d => d.month === month && d.analysis_type === t);
|
|
row[t] = entry?.count || 0;
|
|
}
|
|
return row;
|
|
});
|
|
|
|
return (
|
|
<div className="bg-bg-card/60 border border-primary/15 rounded-2xl p-6">
|
|
<h4 className="text-md font-semibold text-text-primary mb-4">Analysis Types Over Time</h4>
|
|
<ResponsiveContainer width="100%" height={300}>
|
|
<BarChart data={pivoted}>
|
|
<XAxis dataKey="month" stroke={chartTheme.axisStroke} fontSize={12} />
|
|
<YAxis stroke={chartTheme.axisStroke} fontSize={12} />
|
|
<Tooltip
|
|
contentStyle={chartTheme.tooltipContentStyle}
|
|
labelStyle={chartTheme.tooltipLabelStyle}
|
|
/>
|
|
<Legend />
|
|
{types.map((type, idx) => (
|
|
<Bar
|
|
key={type}
|
|
dataKey={type}
|
|
stackId="types"
|
|
fill={COLORS[idx % COLORS.length]}
|
|
name={type}
|
|
/>
|
|
))}
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
);
|
|
})()}
|
|
</div>
|
|
|
|
{trendsQuery.data.by_month.length === 0 && (
|
|
<div className="text-text-secondary text-center py-8">
|
|
No trend data available yet. Run analyses over multiple days to see trends.
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function MetricCard({ label, value }: { label: string; value: number }) {
|
|
return (
|
|
<div className="bg-gradient-to-br from-primary/10 to-secondary/10 border border-primary/20 rounded-xl p-5 text-center">
|
|
<div className="text-3xl font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
|
|
{value}
|
|
</div>
|
|
<div className="text-sm text-text-secondary uppercase tracking-wide mt-1">{label}</div>
|
|
</div>
|
|
);
|
|
}
|