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>
222 lines
6.7 KiB
TypeScript
222 lines
6.7 KiB
TypeScript
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
|
|
import type { TokenResponse, User, CompanyAnalysis, BatchAnalysisResult, JobStatus, Analytics } from '../types';
|
|
|
|
const API_BASE_URL = import.meta.env.VITE_API_URL || '/api';
|
|
|
|
const api = axios.create({
|
|
baseURL: API_BASE_URL,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
|
|
// Token management
|
|
let accessToken: string | null = localStorage.getItem('access_token');
|
|
let refreshToken: string | null = localStorage.getItem('refresh_token');
|
|
|
|
export const setTokens = (tokens: TokenResponse) => {
|
|
accessToken = tokens.access_token;
|
|
refreshToken = tokens.refresh_token;
|
|
localStorage.setItem('access_token', tokens.access_token);
|
|
localStorage.setItem('refresh_token', tokens.refresh_token);
|
|
};
|
|
|
|
export const clearTokens = () => {
|
|
accessToken = null;
|
|
refreshToken = null;
|
|
localStorage.removeItem('access_token');
|
|
localStorage.removeItem('refresh_token');
|
|
};
|
|
|
|
export const getAccessToken = () => accessToken;
|
|
|
|
// Request interceptor to add auth header
|
|
api.interceptors.request.use((config: InternalAxiosRequestConfig) => {
|
|
if (accessToken) {
|
|
config.headers.Authorization = `Bearer ${accessToken}`;
|
|
}
|
|
return config;
|
|
});
|
|
|
|
// Response interceptor to handle token refresh
|
|
api.interceptors.response.use(
|
|
(response) => response,
|
|
async (error: AxiosError) => {
|
|
const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };
|
|
|
|
if (error.response?.status === 401 && !originalRequest._retry && refreshToken) {
|
|
originalRequest._retry = true;
|
|
|
|
try {
|
|
const response = await axios.post<TokenResponse>(`${API_BASE_URL}/auth/refresh`, {
|
|
refresh_token: refreshToken,
|
|
});
|
|
|
|
setTokens(response.data);
|
|
originalRequest.headers.Authorization = `Bearer ${response.data.access_token}`;
|
|
|
|
return api(originalRequest);
|
|
} catch {
|
|
clearTokens();
|
|
window.location.href = '/login';
|
|
}
|
|
}
|
|
|
|
return Promise.reject(error);
|
|
}
|
|
);
|
|
|
|
// Auth API
|
|
export const authApi = {
|
|
register: async (email: string, password: string): Promise<User> => {
|
|
const response = await api.post<User>('/auth/register', { email, password });
|
|
return response.data;
|
|
},
|
|
|
|
login: async (email: string, password: string): Promise<TokenResponse> => {
|
|
const response = await api.post<TokenResponse>('/auth/login', { email, password });
|
|
setTokens(response.data);
|
|
return response.data;
|
|
},
|
|
|
|
getMe: async (): Promise<User> => {
|
|
const response = await api.get<User>('/auth/me');
|
|
return response.data;
|
|
},
|
|
|
|
logout: () => {
|
|
clearTokens();
|
|
},
|
|
};
|
|
|
|
// 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, 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, 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, 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;
|
|
},
|
|
|
|
listJobs: async (status?: string, limit = 10): Promise<JobStatus[]> => {
|
|
const params = new URLSearchParams();
|
|
if (status) params.append('status', status);
|
|
params.append('limit', limit.toString());
|
|
const response = await api.get<JobStatus[]>(`/jobs?${params}`);
|
|
return response.data;
|
|
},
|
|
};
|
|
|
|
// Export API
|
|
export const exportApi = {
|
|
exportCsv: async (companyName: string): Promise<void> => {
|
|
const response = await api.get(`/export/${encodeURIComponent(companyName)}`, {
|
|
responseType: 'blob',
|
|
});
|
|
const url = window.URL.createObjectURL(new Blob([response.data]));
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
link.setAttribute('download', `sparc_${companyName.toLowerCase().replace(/\s+/g, '_')}_export.csv`);
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
link.remove();
|
|
window.URL.revokeObjectURL(url);
|
|
},
|
|
exportPdf: async (companyName: string): Promise<void> => {
|
|
const response = await api.get(`/export/${encodeURIComponent(companyName)}/pdf`, {
|
|
responseType: 'blob',
|
|
});
|
|
const safeName = companyName.toLowerCase().replace(/\s+/g, '_');
|
|
const date = new Date().toISOString().split('T')[0];
|
|
const url = window.URL.createObjectURL(new Blob([response.data], { type: 'application/pdf' }));
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
link.setAttribute('download', `${safeName}-analysis-${date}.pdf`);
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
link.remove();
|
|
window.URL.revokeObjectURL(url);
|
|
},
|
|
};
|
|
|
|
// Analytics API
|
|
export interface TrendData {
|
|
by_month: Array<{ month: string; company_name: string; count: number }>;
|
|
by_type_over_time: Array<{ month: string; analysis_type: string; count: number }>;
|
|
period_days: number;
|
|
}
|
|
|
|
export const analyticsApi = {
|
|
getAnalytics: async (days = 30): Promise<Analytics> => {
|
|
const response = await api.get<Analytics>(`/analytics?days=${days}`);
|
|
return response.data;
|
|
},
|
|
|
|
getTrends: async (days = 90): Promise<TrendData> => {
|
|
const response = await api.get<TrendData>(`/analytics/trends?days=${days}`);
|
|
return response.data;
|
|
},
|
|
};
|
|
|
|
// Admin API
|
|
export const adminApi = {
|
|
listUsers: async (limit = 100, offset = 0): Promise<User[]> => {
|
|
const response = await api.get<User[]>(`/admin/users?limit=${limit}&offset=${offset}`);
|
|
return response.data;
|
|
},
|
|
|
|
updateUserRole: async (userId: number, role: 'admin' | 'user'): Promise<User> => {
|
|
const response = await api.patch<User>(`/admin/users/${userId}/role`, { role });
|
|
return response.data;
|
|
},
|
|
|
|
deleteUser: async (userId: number): Promise<void> => {
|
|
await api.delete(`/admin/users/${userId}`);
|
|
},
|
|
};
|
|
|
|
export default api;
|