forked from 0xWheatyz/SPARC
223d5f7e5d
Thread the optional model parameter through the entire analysis pipeline: - analyzer.py: analyze_company, _analyze_company_safe, analyze_companies, and analyze_single_patent now accept and forward model override - api.py: single company endpoint accepts model query param; batch and async batch endpoints pass request.model through to the analyzer - client.ts: analyzeCompany, analyzeBatch, analyzeBatchAsync accept model; add listModels() to fetch available models from GET /models - Analysis.tsx: add model selector dropdown that loads from /models API - Batch.tsx: add model selector alongside the workers slider Users can now pick a specific LLM (GPT-4o, Claude 3.5, Gemini, etc.) per analysis request, or leave it on the server default. Closes leeworks-agents/SPARC#351 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
290 lines
11 KiB
TypeScript
290 lines
11 KiB
TypeScript
import { useState } from 'react';
|
|
import { useMutation, useQuery } 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 [selectedModel, setSelectedModel] = useState('');
|
|
const [result, setResult] = useState<BatchAnalysisResult | null>(null);
|
|
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set());
|
|
|
|
const modelsQuery = useQuery({
|
|
queryKey: ['models'],
|
|
queryFn: () => analysisApi.listModels(),
|
|
});
|
|
|
|
const mutation = useMutation({
|
|
mutationFn: ({ companies, workers }: { companies: string[]; workers: number }) =>
|
|
analysisApi.analyzeBatch(companies, workers, selectedModel || undefined),
|
|
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>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-text-secondary mb-2">
|
|
LLM Model
|
|
</label>
|
|
<div className="relative">
|
|
<select
|
|
value={selectedModel}
|
|
onChange={(e) => setSelectedModel(e.target.value)}
|
|
className="w-full appearance-none bg-bg-card/80 border border-primary/30 rounded-lg pl-3 pr-8 py-2 text-sm text-text-primary focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all cursor-pointer"
|
|
>
|
|
<option value="">
|
|
{modelsQuery.data ? `Default (${modelsQuery.data.default})` : 'Default'}
|
|
</option>
|
|
{modelsQuery.data?.models.map((m) => (
|
|
<option key={m.id} value={m.id}>
|
|
{m.name} ({m.provider})
|
|
</option>
|
|
))}
|
|
</select>
|
|
<ChevronDown className="absolute right-2 top-1/2 -translate-y-1/2 text-text-secondary pointer-events-none" size={16} />
|
|
</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="bg-error/10 border border-error/20 rounded-xl px-4 py-3">
|
|
<div className="flex items-center gap-2 text-error">
|
|
<AlertCircle size={18} />
|
|
<span className="font-semibold">Batch analysis failed</span>
|
|
</div>
|
|
<p className="text-text-secondary text-sm mt-1 ml-7">
|
|
{mutation.error instanceof Error ? mutation.error.message : 'An unexpected error occurred.'}
|
|
{' '}Check your connection and try again.
|
|
</p>
|
|
<button
|
|
onClick={() => mutation.reset()}
|
|
className="ml-7 mt-2 text-sm text-primary hover:text-primary-dark underline"
|
|
>
|
|
Dismiss
|
|
</button>
|
|
</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>
|
|
);
|
|
}
|