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 && ( + + )} +
+ )} +
+ ); +}