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