feat(frontend): add React dashboard with TypeScript

Add modern React frontend to replace Streamlit dashboard:
- Vite build system with TypeScript
- Tailwind CSS for styling
- Component structure in src/
- Production Dockerfile with nginx
- Development server on port 5173

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-03-14 13:40:52 -04:00
parent 9c98b948d3
commit cb7d7121c5
25 changed files with 1942 additions and 0 deletions
+135
View File
@@ -0,0 +1,135 @@
import { useState } from 'react';
import { useMutation } from '@tanstack/react-query';
import { analysisApi } from '../api/client';
import { Search, CheckCircle, AlertCircle, Clock, FileText } 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">
<h3 className="text-lg font-semibold text-text-primary border-b-2 border-primary/30 pb-2 mb-4">
AI Analysis Results
</h3>
<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>
);
}