From c738f785c3c5b245d294d8417ce0a833d65ca610 Mon Sep 17 00:00:00 2001 From: agent-company Date: Thu, 26 Mar 2026 10:22:14 +0000 Subject: [PATCH] feat: add side-by-side patent portfolio comparison view Add /compare route with two-panel layout for comparing company patent portfolios. Each panel shows patent count, analysis timestamp, and full LLM narrative. The page is responsive (stacks vertically on mobile) and supports URL params (?a=nvidia&b=intel) for shareability. Closes leeworks-agents/SPARC#21 Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/App.tsx | 2 + frontend/src/components/Layout.tsx | 3 +- frontend/src/pages/Compare.tsx | 161 +++++++++++++++++++++++++++++ 3 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 frontend/src/pages/Compare.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c3426cd..e630389 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -10,6 +10,7 @@ import { Batch } from './pages/Batch'; import { AnalyticsPage } from './pages/Analytics'; import { About } from './pages/About'; import { AdminUsers } from './pages/AdminUsers'; +import { Compare } from './pages/Compare'; const queryClient = new QueryClient({ defaultOptions: { @@ -41,6 +42,7 @@ function App() { } /> } /> } /> + } /> } /> {/* Admin routes */} diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 501dc1f..0f5afbf 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -1,6 +1,6 @@ import { Outlet, NavLink, useNavigate } from 'react-router-dom'; import { useAuth } from '../context/AuthContext'; -import { Search, Layers, BarChart3, Info, Users, LogOut } from 'lucide-react'; +import { Search, Layers, BarChart3, Info, Users, LogOut, GitCompareArrows } from 'lucide-react'; export function Layout() { const { user, isAdmin, logout } = useAuth(); @@ -15,6 +15,7 @@ export function Layout() { { to: '/analysis', icon: Search, label: 'Analysis' }, { to: '/batch', icon: Layers, label: 'Batch' }, { to: '/analytics', icon: BarChart3, label: 'Analytics' }, + { to: '/compare', icon: GitCompareArrows, label: 'Compare' }, { to: '/about', icon: Info, label: 'About' }, ]; diff --git a/frontend/src/pages/Compare.tsx b/frontend/src/pages/Compare.tsx new file mode 100644 index 0000000..eef3e53 --- /dev/null +++ b/frontend/src/pages/Compare.tsx @@ -0,0 +1,161 @@ +import { useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { useQuery } from '@tanstack/react-query'; +import { analysisApi } from '../api/client'; +import { GitCompareArrows, AlertCircle, FileText, Clock } from 'lucide-react'; +import type { CompanyAnalysis } from '../types'; + +function CompanyPanel({ data, isLoading, isError }: { data?: CompanyAnalysis; isLoading: boolean; isError: boolean }) { + if (isLoading) { + return ( +
+
+
+
+
+
+
+
+ ); + } + + if (isError) { + return ( +
+
+ + Failed to load analysis. Check the company name and try again. +
+
+ ); + } + + if (!data) return null; + + return ( +
+

+ {data.company_name.toUpperCase()} +

+ +
+
+ +
{data.patent_count}
+
Patents
+
+
+ +
+ {new Date(data.timestamp).toLocaleDateString()} +
+
Analyzed
+
+
+ + {data.success && data.analysis ? ( +
+ {data.analysis} +
+ ) : ( +
{data.error || 'Analysis not available'}
+ )} +
+ ); +} + +export function Compare() { + const [searchParams, setSearchParams] = useSearchParams(); + const [companyA, setCompanyA] = useState(searchParams.get('a') || ''); + const [companyB, setCompanyB] = useState(searchParams.get('b') || ''); + + const queryA = searchParams.get('a') || ''; + const queryB = searchParams.get('b') || ''; + + const resultA = useQuery({ + queryKey: ['analyze', queryA], + queryFn: () => analysisApi.analyzeCompany(queryA), + enabled: !!queryA, + }); + + const resultB = useQuery({ + queryKey: ['analyze', queryB], + queryFn: () => analysisApi.analyzeCompany(queryB), + enabled: !!queryB, + }); + + const handleCompare = (e: React.FormEvent) => { + e.preventDefault(); + const a = companyA.trim(); + const b = companyB.trim(); + if (a && b) { + setSearchParams({ a, b }); + } + }; + + return ( +
+ {/* Header */} +
+

+ Portfolio Comparison +

+

+ Compare patent portfolios of two companies side by side. +

+
+ + {/* Input Form */} +
+
+ + setCompanyA(e.target.value)} + placeholder="e.g. nvidia" + className="w-full bg-bg-card/80 border border-primary/30 rounded-xl px-4 py-2.5 text-text-primary placeholder-text-secondary/50 focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all" + /> +
+
+ + setCompanyB(e.target.value)} + placeholder="e.g. intel" + className="w-full bg-bg-card/80 border border-primary/30 rounded-xl px-4 py-2.5 text-text-primary placeholder-text-secondary/50 focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all" + /> +
+ +
+ + {/* Comparison Panels */} + {(queryA || queryB) && ( +
+ {queryA && ( + + )} + {queryB && ( + + )} +
+ )} +
+ ); +}