forked from 0xWheatyz/SPARC
Add LLM-based patent classification tagging by technology domain
- Add classify_patent_tags() to LLMAnalyzer with canonical tag list (ai, semiconductors, materials, biotech, networking, other) - Add patent_tags TEXT[] column to patents table with GIN index - Run classification automatically in the analysis pipeline after patent processing; persist tags via update_patent_tags() - Include tags in CompanyAnalysisResult and API response models - Add ?tags= filter to GET /analyze/batch endpoint - Add GET /analytics/tags endpoint for tag distribution data - Add tag filter controls and distribution chart to Analytics page - Add 12 unit tests covering classification, DB storage, and caching Closes leeworks-agents/SPARC#1672 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -199,8 +199,18 @@ export const analyticsApi = {
|
||||
const response = await api.get<TrendData>(`/analytics/trends?days=${days}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getTagDistribution: async (): Promise<TagDistribution> => {
|
||||
const response = await api.get<TagDistribution>('/analytics/tags');
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
export interface TagDistribution {
|
||||
by_tag: Array<{ tag: string; count: number }>;
|
||||
canonical_tags: string[];
|
||||
}
|
||||
|
||||
// Admin API
|
||||
export const adminApi = {
|
||||
listUsers: async (limit = 100, offset = 0): Promise<User[]> => {
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
import { useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { analyticsApi } from '../api/client';
|
||||
import { AlertCircle, Database } from 'lucide-react';
|
||||
import { AlertCircle, Database, Tag } 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'];
|
||||
|
||||
const TAG_COLORS: Record<string, string> = {
|
||||
ai: '#6366f1',
|
||||
semiconductors: '#0ea5e9',
|
||||
materials: '#10b981',
|
||||
biotech: '#f59e0b',
|
||||
networking: '#ec4899',
|
||||
other: '#8b5cf6',
|
||||
};
|
||||
|
||||
export function AnalyticsPage() {
|
||||
const [days, setDays] = useState(30);
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||
const chartTheme = useChartTheme();
|
||||
|
||||
const { data, isLoading, isError, refetch } = useQuery({
|
||||
@@ -21,6 +31,17 @@ export function AnalyticsPage() {
|
||||
queryFn: () => analyticsApi.getTrends(days),
|
||||
});
|
||||
|
||||
const tagQuery = useQuery({
|
||||
queryKey: ['analytics-tags'],
|
||||
queryFn: () => analyticsApi.getTagDistribution(),
|
||||
});
|
||||
|
||||
const toggleTag = (tag: string) => {
|
||||
setSelectedTags((prev) =>
|
||||
prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag]
|
||||
);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -107,6 +128,13 @@ export function AnalyticsPage() {
|
||||
count: t.count,
|
||||
}));
|
||||
|
||||
const tagData = tagQuery.data?.by_tag?.map((t) => ({
|
||||
name: t.tag,
|
||||
count: t.count,
|
||||
})) || [];
|
||||
|
||||
const canonicalTags = tagQuery.data?.canonical_tags || [];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
@@ -131,6 +159,50 @@ export function AnalyticsPage() {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Tag Filter Controls */}
|
||||
{canonicalTags.length > 0 && (
|
||||
<div className="bg-bg-card/60 border border-primary/15 rounded-2xl p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Tag size={16} className="text-primary" />
|
||||
<span className="text-sm font-semibold text-text-primary">Filter by Technology Domain</span>
|
||||
{selectedTags.length > 0 && (
|
||||
<button
|
||||
onClick={() => setSelectedTags([])}
|
||||
className="ml-auto text-xs text-text-secondary hover:text-primary transition-colors"
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{canonicalTags.map((tag) => {
|
||||
const isActive = selectedTags.includes(tag);
|
||||
const color = TAG_COLORS[tag] || '#8b5cf6';
|
||||
const tagCount = tagQuery.data?.by_tag?.find((t) => t.tag === tag)?.count || 0;
|
||||
return (
|
||||
<button
|
||||
key={tag}
|
||||
onClick={() => toggleTag(tag)}
|
||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-all ${
|
||||
isActive
|
||||
? 'text-white shadow-md'
|
||||
: 'bg-bg-card/80 text-text-secondary border border-primary/20 hover:border-primary/40'
|
||||
}`}
|
||||
style={isActive ? { backgroundColor: color } : {}}
|
||||
>
|
||||
{tag}
|
||||
{tagCount > 0 && (
|
||||
<span className={`ml-1.5 text-xs ${isActive ? 'opacity-80' : 'opacity-60'}`}>
|
||||
({tagCount})
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Summary Metrics */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<MetricCard label="Total Analyses" value={data.total_messages} />
|
||||
@@ -187,6 +259,28 @@ export function AnalyticsPage() {
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Bar Chart - Technology Domain Tags */}
|
||||
{tagData.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">Patents by Technology Domain</h3>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<BarChart data={tagData}>
|
||||
<XAxis dataKey="name" stroke={chartTheme.axisStroke} fontSize={12} />
|
||||
<YAxis stroke={chartTheme.axisStroke} fontSize={12} />
|
||||
<Tooltip
|
||||
contentStyle={chartTheme.tooltipContentStyle}
|
||||
labelStyle={chartTheme.tooltipLabelStyle}
|
||||
/>
|
||||
<Bar dataKey="count" radius={[4, 4, 0, 0]}>
|
||||
{tagData.map((entry, index) => (
|
||||
<Cell key={`tag-cell-${index}`} fill={TAG_COLORS[entry.name] || COLORS[index % COLORS.length]} />
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Trend Charts */}
|
||||
|
||||
Reference in New Issue
Block a user