forked from 0xWheatyz/SPARC
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,248 @@
|
||||
import { useState } from 'react';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { analysisApi } from '../api/client';
|
||||
import { Rocket, CheckCircle, AlertCircle, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Cell } from 'recharts';
|
||||
import type { BatchAnalysisResult } from '../types';
|
||||
|
||||
export function Batch() {
|
||||
const [companiesInput, setCompaniesInput] = useState('');
|
||||
const [maxWorkers, setMaxWorkers] = useState(3);
|
||||
const [result, setResult] = useState<BatchAnalysisResult | null>(null);
|
||||
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set());
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: ({ companies, workers }: { companies: string[]; workers: number }) =>
|
||||
analysisApi.analyzeBatch(companies, workers),
|
||||
onSuccess: (data) => setResult(data),
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const companies = companiesInput
|
||||
.split(/[,\n]/)
|
||||
.map((c) => c.trim())
|
||||
.filter((c) => c.length > 0);
|
||||
|
||||
if (companies.length > 0) {
|
||||
mutation.mutate({ companies, workers: maxWorkers });
|
||||
}
|
||||
};
|
||||
|
||||
const toggleExpand = (company: string) => {
|
||||
const newExpanded = new Set(expandedItems);
|
||||
if (newExpanded.has(company)) {
|
||||
newExpanded.delete(company);
|
||||
} else {
|
||||
newExpanded.add(company);
|
||||
}
|
||||
setExpandedItems(newExpanded);
|
||||
};
|
||||
|
||||
const chartData = result?.results.map((r) => ({
|
||||
name: r.company_name.toUpperCase(),
|
||||
patents: r.patent_count,
|
||||
success: r.success,
|
||||
}));
|
||||
|
||||
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">
|
||||
Batch Company Analysis
|
||||
</h2>
|
||||
<p className="text-text-secondary">
|
||||
Analyze multiple companies simultaneously for comparative insights.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Input Form */}
|
||||
<form onSubmit={handleSubmit} className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="md:col-span-2">
|
||||
<textarea
|
||||
value={companiesInput}
|
||||
onChange={(e) => setCompaniesInput(e.target.value)}
|
||||
placeholder="Enter company names (one per line or comma-separated): nvidia amd intel qualcomm"
|
||||
rows={6}
|
||||
className="w-full bg-bg-card/80 border border-primary/30 rounded-xl px-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 resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-secondary mb-2">
|
||||
Concurrent Workers
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min={1}
|
||||
max={5}
|
||||
value={maxWorkers}
|
||||
onChange={(e) => setMaxWorkers(Number(e.target.value))}
|
||||
className="w-full accent-primary"
|
||||
/>
|
||||
<div className="text-center text-text-primary font-semibold">{maxWorkers}</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={mutation.isPending || !companiesInput.trim()}
|
||||
className="w-full 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 justify-center gap-2"
|
||||
>
|
||||
{mutation.isPending ? (
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-t-2 border-b-2 border-white"></div>
|
||||
) : (
|
||||
<>
|
||||
<Rocket size={18} />
|
||||
Run Batch Analysis
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Progress */}
|
||||
{mutation.isPending && (
|
||||
<div className="bg-bg-card/60 border border-primary/15 rounded-xl p-4">
|
||||
<div className="flex items-center gap-2 text-secondary">
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-t-2 border-b-2 border-secondary"></div>
|
||||
<span>Analyzing companies...</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 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>Batch analysis failed. Please try again.</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Results */}
|
||||
{result && (
|
||||
<div className="space-y-6">
|
||||
{/* Summary Metrics */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-text-primary border-b-2 border-primary/30 pb-2 mb-4">
|
||||
Results Summary
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<SummaryCard label="Total Companies" value={result.total_companies} />
|
||||
<SummaryCard label="Successful" value={result.successful} color="success" />
|
||||
<SummaryCard label="Failed" value={result.failed} color="error" />
|
||||
<SummaryCard
|
||||
label="Success Rate"
|
||||
value={`${Math.round((result.successful / result.total_companies) * 100)}%`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chart */}
|
||||
{chartData && chartData.length > 0 && (
|
||||
<div className="bg-bg-card/60 border border-primary/15 rounded-2xl p-6">
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<BarChart data={chartData}>
|
||||
<XAxis dataKey="name" stroke="#94a3b8" fontSize={12} />
|
||||
<YAxis stroke="#94a3b8" fontSize={12} />
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: '#1e293b',
|
||||
border: '1px solid rgba(99, 102, 241, 0.3)',
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
labelStyle={{ color: '#f8fafc' }}
|
||||
/>
|
||||
<Bar dataKey="patents" radius={[4, 4, 0, 0]}>
|
||||
{chartData.map((entry, index) => (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
fill={entry.success ? '#10b981' : '#ef4444'}
|
||||
/>
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Detailed Results */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-text-primary border-b-2 border-primary/30 pb-2 mb-4">
|
||||
Detailed Results
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{result.results.map((r) => (
|
||||
<div
|
||||
key={r.company_name}
|
||||
className="bg-bg-card/60 border border-primary/15 rounded-xl overflow-hidden"
|
||||
>
|
||||
<button
|
||||
onClick={() => toggleExpand(r.company_name)}
|
||||
className="w-full flex items-center justify-between p-4 hover:bg-bg-card-hover transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
{r.success ? (
|
||||
<CheckCircle className="text-success" size={20} />
|
||||
) : (
|
||||
<AlertCircle className="text-error" size={20} />
|
||||
)}
|
||||
<span className="font-semibold text-text-primary">
|
||||
{r.company_name.toUpperCase()}
|
||||
</span>
|
||||
<span className="text-text-secondary">
|
||||
{r.patent_count} patents
|
||||
</span>
|
||||
</div>
|
||||
{expandedItems.has(r.company_name) ? (
|
||||
<ChevronUp className="text-text-secondary" size={20} />
|
||||
) : (
|
||||
<ChevronDown className="text-text-secondary" size={20} />
|
||||
)}
|
||||
</button>
|
||||
{expandedItems.has(r.company_name) && (
|
||||
<div className="border-t border-primary/10 p-4 bg-bg-dark/40">
|
||||
{r.success ? (
|
||||
<div className="text-text-primary whitespace-pre-wrap leading-relaxed">
|
||||
{r.analysis}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-error">{r.error}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SummaryCard({
|
||||
label,
|
||||
value,
|
||||
color,
|
||||
}: {
|
||||
label: string;
|
||||
value: number | string;
|
||||
color?: 'success' | 'error';
|
||||
}) {
|
||||
const colorClass = color === 'success' ? 'text-success' : color === 'error' ? 'text-error' : '';
|
||||
|
||||
return (
|
||||
<div className="bg-gradient-to-br from-primary/10 to-secondary/10 border border-primary/20 rounded-xl p-4 text-center">
|
||||
<div
|
||||
className={`text-2xl font-bold ${
|
||||
colorClass || '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