feat: add model picker to Analysis and Batch pages with full backend wiring

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>
This commit is contained in:
agent-company
2026-03-27 16:13:00 +00:00
parent 514e274fdb
commit 223d5f7e5d
5 changed files with 137 additions and 43 deletions
+28 -4
View File
@@ -89,29 +89,53 @@ export const authApi = {
},
};
// Model types
export interface ModelInfo {
id: string;
name: string;
provider: string;
}
export interface ModelsResponse {
models: ModelInfo[];
default: string;
}
// Analysis API
export const analysisApi = {
analyzeCompany: async (companyName: string): Promise<CompanyAnalysis> => {
const response = await api.get<CompanyAnalysis>(`/analyze/${encodeURIComponent(companyName)}`);
analyzeCompany: async (companyName: string, model?: string): Promise<CompanyAnalysis> => {
const params = new URLSearchParams();
if (model) params.append('model', model);
const qs = params.toString();
const response = await api.get<CompanyAnalysis>(
`/analyze/${encodeURIComponent(companyName)}${qs ? `?${qs}` : ''}`
);
return response.data;
},
analyzeBatch: async (companies: string[], maxWorkers = 3): Promise<BatchAnalysisResult> => {
analyzeBatch: async (companies: string[], maxWorkers = 3, model?: string): Promise<BatchAnalysisResult> => {
const response = await api.post<BatchAnalysisResult>('/analyze/batch', {
companies,
max_workers: maxWorkers,
...(model ? { model } : {}),
});
return response.data;
},
analyzeBatchAsync: async (companies: string[], maxWorkers = 3): Promise<JobStatus> => {
analyzeBatchAsync: async (companies: string[], maxWorkers = 3, model?: string): Promise<JobStatus> => {
const response = await api.post<JobStatus>('/analyze/batch/async', {
companies,
max_workers: maxWorkers,
...(model ? { model } : {}),
});
return response.data;
},
listModels: async (): Promise<ModelsResponse> => {
const response = await api.get<ModelsResponse>('/models');
return response.data;
},
getJobStatus: async (jobId: string): Promise<JobStatus> => {
const response = await api.get<JobStatus>(`/jobs/${jobId}`);
return response.data;
+59 -27
View File
@@ -1,15 +1,21 @@
import { useState } from 'react';
import { useMutation } from '@tanstack/react-query';
import { useMutation, useQuery } from '@tanstack/react-query';
import { analysisApi, exportApi } from '../api/client';
import { Search, CheckCircle, AlertCircle, Clock, FileText, Download } from 'lucide-react';
import { Search, CheckCircle, AlertCircle, Clock, FileText, Download, ChevronDown } from 'lucide-react';
import type { CompanyAnalysis } from '../types';
export function Analysis() {
const [companyName, setCompanyName] = useState('');
const [selectedModel, setSelectedModel] = useState('');
const [result, setResult] = useState<CompanyAnalysis | null>(null);
const modelsQuery = useQuery({
queryKey: ['models'],
queryFn: () => analysisApi.listModels(),
});
const mutation = useMutation({
mutationFn: (name: string) => analysisApi.analyzeCompany(name),
mutationFn: (name: string) => analysisApi.analyzeCompany(name, selectedModel || undefined),
onSuccess: (data) => setResult(data),
});
@@ -33,31 +39,57 @@ export function Analysis() {
</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"
/>
<form onSubmit={handleSubmit} className="space-y-4">
<div 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>
</div>
{/* Model Selector */}
<div className="flex items-center gap-3">
<label className="text-sm font-medium text-text-secondary whitespace-nowrap">
LLM Model
</label>
<div className="relative flex-1 max-w-xs">
<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 || !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 */}
+31 -2
View File
@@ -1,5 +1,5 @@
import { useState } from 'react';
import { useMutation } from '@tanstack/react-query';
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';
@@ -8,12 +8,18 @@ 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),
analysisApi.analyzeBatch(companies, workers, selectedModel || undefined),
onSuccess: (data) => setResult(data),
});
@@ -85,6 +91,29 @@ export function Batch() {
<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()}