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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user