|
|
|
@@ -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 (
|
|
|
|
|
<div className="bg-bg-card/60 border border-primary/15 rounded-2xl p-6 animate-pulse">
|
|
|
|
|
<div className="h-6 w-32 bg-primary/20 rounded mb-4" />
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<div className="h-4 bg-primary/10 rounded w-full" />
|
|
|
|
|
<div className="h-4 bg-primary/10 rounded w-3/4" />
|
|
|
|
|
<div className="h-4 bg-primary/10 rounded w-5/6" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isError) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="bg-error/10 border border-error/20 rounded-2xl p-6">
|
|
|
|
|
<div className="flex items-center gap-2 text-error">
|
|
|
|
|
<AlertCircle size={18} />
|
|
|
|
|
<span>Failed to load analysis. Check the company name and try again.</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!data) return null;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="bg-bg-card/60 border border-primary/15 rounded-2xl p-6 space-y-4">
|
|
|
|
|
<h3 className="text-lg font-bold text-text-primary border-b-2 border-primary/30 pb-2">
|
|
|
|
|
{data.company_name.toUpperCase()}
|
|
|
|
|
</h3>
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
|
|
|
<div className="bg-primary/10 rounded-lg p-3 text-center">
|
|
|
|
|
<FileText className="mx-auto mb-1 text-primary" size={18} />
|
|
|
|
|
<div className="text-xl font-bold text-text-primary">{data.patent_count}</div>
|
|
|
|
|
<div className="text-xs text-text-secondary uppercase">Patents</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="bg-primary/10 rounded-lg p-3 text-center">
|
|
|
|
|
<Clock className="mx-auto mb-1 text-primary" size={18} />
|
|
|
|
|
<div className="text-sm font-medium text-text-primary">
|
|
|
|
|
{new Date(data.timestamp).toLocaleDateString()}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-xs text-text-secondary uppercase">Analyzed</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{data.success && data.analysis ? (
|
|
|
|
|
<div className="text-text-primary whitespace-pre-wrap leading-relaxed text-sm">
|
|
|
|
|
{data.analysis}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="text-error text-sm">{data.error || 'Analysis not available'}</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 (
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
{/* Header */}
|
|
|
|
|
<div>
|
|
|
|
|
<h2 className="text-xl font-semibold text-text-primary border-b-2 border-primary/30 pb-2 mb-2">
|
|
|
|
|
Portfolio Comparison
|
|
|
|
|
</h2>
|
|
|
|
|
<p className="text-text-secondary">
|
|
|
|
|
Compare patent portfolios of two companies side by side.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Input Form */}
|
|
|
|
|
<form onSubmit={handleCompare} className="flex flex-col sm:flex-row gap-3 items-end">
|
|
|
|
|
<div className="flex-1">
|
|
|
|
|
<label className="block text-sm font-medium text-text-secondary mb-1">Company A</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={companyA}
|
|
|
|
|
onChange={(e) => 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"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex-1">
|
|
|
|
|
<label className="block text-sm font-medium text-text-secondary mb-1">Company B</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={companyB}
|
|
|
|
|
onChange={(e) => 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"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
type="submit"
|
|
|
|
|
disabled={!companyA.trim() || !companyB.trim() || resultA.isLoading || resultB.isLoading}
|
|
|
|
|
className="bg-gradient-to-r from-primary to-primary-dark text-white font-semibold py-2.5 px-6 rounded-xl hover:shadow-lg hover:shadow-primary/30 transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
|
|
|
|
>
|
|
|
|
|
<GitCompareArrows size={18} />
|
|
|
|
|
Compare
|
|
|
|
|
</button>
|
|
|
|
|
</form>
|
|
|
|
|
|
|
|
|
|
{/* Comparison Panels */}
|
|
|
|
|
{(queryA || queryB) && (
|
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
|
|
|
{queryA && (
|
|
|
|
|
<CompanyPanel
|
|
|
|
|
data={resultA.data}
|
|
|
|
|
isLoading={resultA.isLoading}
|
|
|
|
|
isError={resultA.isError}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
{queryB && (
|
|
|
|
|
<CompanyPanel
|
|
|
|
|
data={resultB.data}
|
|
|
|
|
isLoading={resultB.isLoading}
|
|
|
|
|
isError={resultB.isError}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|