diff --git a/SPARC/api.py b/SPARC/api.py index a78c132..bc58fd0 100644 --- a/SPARC/api.py +++ b/SPARC/api.py @@ -389,6 +389,78 @@ async def get_analytics( ) +@app.get("/analytics/trends", tags=["Analytics"]) +async def get_analytics_trends( + days: int = Query(default=90, ge=7, le=365), + _: UserResponse = Depends(get_current_user), +): + """Get trend data for patent analysis over time. + + Returns two datasets: + - ``by_month``: analysis count per company per month + - ``by_type_over_time``: analysis type distribution per month + + Args: + days: Number of days to look back (default 90) + + Returns: + Trend data suitable for time-series and distribution charts + """ + db = get_db_client() + + with db.get_conn() as conn: + with conn.cursor() as cur: + # Analyses per company per month + cur.execute( + """ + SELECT + TO_CHAR(timestamp, 'YYYY-MM') AS month, + company_name, + COUNT(*) AS count + FROM llm_messages + WHERE timestamp >= NOW() - INTERVAL '%s days' + AND is_cached = FALSE + AND company_name IS NOT NULL + GROUP BY month, company_name + ORDER BY month + """, + (days,), + ) + by_month_rows = cur.fetchall() + + # Analysis type distribution per month + cur.execute( + """ + SELECT + TO_CHAR(timestamp, 'YYYY-MM') AS month, + analysis_type, + COUNT(*) AS count + FROM llm_messages + WHERE timestamp >= NOW() - INTERVAL '%s days' + AND is_cached = FALSE + GROUP BY month, analysis_type + ORDER BY month + """, + (days,), + ) + by_type_rows = cur.fetchall() + + by_month = [ + {"month": row[0], "company_name": row[1], "count": row[2]} + for row in by_month_rows + ] + by_type_over_time = [ + {"month": row[0], "analysis_type": row[1], "count": row[2]} + for row in by_type_rows + ] + + return { + "by_month": by_month, + "by_type_over_time": by_type_over_time, + "period_days": days, + } + + # ============== System Endpoints ============== diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 037d59c..7db5226 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -127,11 +127,22 @@ export const analysisApi = { }; // Analytics API +export interface TrendData { + by_month: Array<{ month: string; company_name: string; count: number }>; + by_type_over_time: Array<{ month: string; analysis_type: string; count: number }>; + period_days: number; +} + export const analyticsApi = { getAnalytics: async (days = 30): Promise => { const response = await api.get(`/analytics?days=${days}`); return response.data; }, + + getTrends: async (days = 90): Promise => { + const response = await api.get(`/analytics/trends?days=${days}`); + return response.data; + }, }; // Admin API diff --git a/frontend/src/pages/Analytics.tsx b/frontend/src/pages/Analytics.tsx index 19f4aff..c3bd31a 100644 --- a/frontend/src/pages/Analytics.tsx +++ b/frontend/src/pages/Analytics.tsx @@ -2,7 +2,7 @@ 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, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend } from 'recharts'; +import { PieChart, Pie, Cell, BarChart, Bar, LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend } from 'recharts'; const COLORS = ['#6366f1', '#0ea5e9', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#14b8a6']; @@ -14,6 +14,11 @@ export function AnalyticsPage() { queryFn: () => analyticsApi.getAnalytics(days), }); + const trendsQuery = useQuery({ + queryKey: ['analytics-trends', days], + queryFn: () => analyticsApi.getTrends(days), + }); + if (isLoading) { return (
@@ -163,6 +168,114 @@ export function AnalyticsPage() {
)} + + {/* Trend Charts */} + {trendsQuery.data && ( +
+

+ Trends Over Time +

+ +
+ {/* 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 = { 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 ( +
+

Analyses per Company Over Time

+ + + + + + + {companies.map((company, idx) => ( + + ))} + + +
+ ); + })()} + + {/* 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 = { 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 ( +
+

Analysis Types Over Time

+ + + + + + + {types.map((type, idx) => ( + + ))} + + +
+ ); + })()} +
+ + {trendsQuery.data.by_month.length === 0 && ( +
+ No trend data available yet. Run analyses over multiple days to see trends. +
+ )} +
+ )} ); }