1bd9dccdb8
Add GET /export/{company_name} backend endpoint that returns analysis
records as a downloadable CSV file. Add Export CSV button to the
Analysis page that triggers the download via the API.
Closes leeworks-agents/SPARC#20
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
145 lines
5.6 KiB
TypeScript
145 lines
5.6 KiB
TypeScript
import { useState } from 'react';
|
|
import { useMutation } from '@tanstack/react-query';
|
|
import { analysisApi, exportApi } from '../api/client';
|
|
import { Search, CheckCircle, AlertCircle, Clock, FileText, Download } from 'lucide-react';
|
|
import type { CompanyAnalysis } from '../types';
|
|
|
|
export function Analysis() {
|
|
const [companyName, setCompanyName] = useState('');
|
|
const [result, setResult] = useState<CompanyAnalysis | null>(null);
|
|
|
|
const mutation = useMutation({
|
|
mutationFn: (name: string) => analysisApi.analyzeCompany(name),
|
|
onSuccess: (data) => setResult(data),
|
|
});
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (companyName.trim()) {
|
|
mutation.mutate(companyName.trim());
|
|
}
|
|
};
|
|
|
|
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">
|
|
Single Company Analysis
|
|
</h2>
|
|
<p className="text-text-secondary">
|
|
Analyze a company's patent portfolio using AI-powered insights.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Search Form */}
|
|
<form onSubmit={handleSubmit} className="flex gap-4">
|
|
<div className="flex-1 relative">
|
|
<Search className="absolute left-4 top-1/2 -translate-y-1/2 text-text-secondary" size={18} />
|
|
<input
|
|
type="text"
|
|
value={companyName}
|
|
onChange={(e) => setCompanyName(e.target.value)}
|
|
placeholder="Enter company name (e.g., nvidia, intel, amd)"
|
|
className="w-full bg-bg-card/80 border border-primary/30 rounded-xl pl-12 pr-4 py-3 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={mutation.isPending || !companyName.trim()}
|
|
className="bg-gradient-to-r from-primary to-primary-dark text-white font-semibold py-3 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"
|
|
>
|
|
{mutation.isPending ? (
|
|
<div className="animate-spin rounded-full h-5 w-5 border-t-2 border-b-2 border-white"></div>
|
|
) : (
|
|
<>
|
|
<Search size={18} />
|
|
Analyze
|
|
</>
|
|
)}
|
|
</button>
|
|
</form>
|
|
|
|
{/* Error */}
|
|
{mutation.isError && (
|
|
<div className="flex items-center gap-2 bg-error/10 border border-error/20 text-error rounded-xl px-4 py-3">
|
|
<AlertCircle size={18} />
|
|
<span>Analysis failed. Please try again.</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Results */}
|
|
{result && (
|
|
<div className="space-y-6">
|
|
{/* Success/Failure Status */}
|
|
{result.success ? (
|
|
<div className="flex items-center gap-2 bg-success/10 border border-success/20 text-success rounded-xl px-4 py-3">
|
|
<CheckCircle size={18} />
|
|
<span>Analysis complete for {result.company_name.toUpperCase()}</span>
|
|
</div>
|
|
) : (
|
|
<div className="flex items-center gap-2 bg-error/10 border border-error/20 text-error rounded-xl px-4 py-3">
|
|
<AlertCircle size={18} />
|
|
<span>Analysis failed: {result.error}</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Metrics */}
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<MetricCard
|
|
icon={FileText}
|
|
label="Patents Found"
|
|
value={result.patent_count.toString()}
|
|
/>
|
|
<MetricCard
|
|
icon={CheckCircle}
|
|
label="Analysis Status"
|
|
value={result.success ? 'Complete' : 'Failed'}
|
|
/>
|
|
<MetricCard
|
|
icon={Clock}
|
|
label="Timestamp"
|
|
value={new Date(result.timestamp).toLocaleTimeString()}
|
|
/>
|
|
</div>
|
|
|
|
{/* Analysis Content */}
|
|
{result.success && result.analysis && (
|
|
<div className="bg-bg-card/60 backdrop-blur-lg border border-primary/15 rounded-2xl p-6">
|
|
<div className="flex items-center justify-between border-b-2 border-primary/30 pb-2 mb-4">
|
|
<h3 className="text-lg font-semibold text-text-primary">
|
|
AI Analysis Results
|
|
</h3>
|
|
<button
|
|
onClick={() => exportApi.exportCsv(result.company_name)}
|
|
className="flex items-center gap-2 text-sm bg-primary/20 hover:bg-primary/30 text-primary font-medium px-3 py-1.5 rounded-lg transition-colors"
|
|
>
|
|
<Download size={14} />
|
|
Export CSV
|
|
</button>
|
|
</div>
|
|
<div className="prose prose-invert max-w-none">
|
|
<div className="text-text-primary whitespace-pre-wrap leading-relaxed">
|
|
{result.analysis}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function MetricCard({ icon: Icon, label, value }: { icon: typeof FileText; label: string; value: string }) {
|
|
return (
|
|
<div className="bg-gradient-to-br from-primary/10 to-secondary/10 border border-primary/20 rounded-xl p-5 text-center">
|
|
<Icon className="mx-auto mb-2 text-primary" size={24} />
|
|
<div className="text-2xl font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
|
|
{value}
|
|
</div>
|
|
<div className="text-sm text-text-secondary uppercase tracking-wide mt-1">{label}</div>
|
|
</div>
|
|
);
|
|
}
|