/** * API client for protein visualization backend * Connects to FastAPI backend (default: http://localhost:8000) */ import axios from 'axios'; import type { FilterParams, FilterOptions, DataResponse, SummaryStats, ChainMetadata } from '../types/protein'; // API base URL - uses Vite proxy in development const API_BASE_URL = import.meta.env.VITE_PROTEIN_API_BASE || '/api'; const api = axios.create({ baseURL: API_BASE_URL, headers: { 'Content-Type': 'application/json', }, }); // Enhanced error handling with diagnostics api.interceptors.response.use( (response) => response, async (error) => { const url = error.config?.url || 'unknown'; const method = error.config?.method?.toUpperCase() || 'REQUEST'; const status = error.response?.status || 'NO_RESPONSE'; const statusText = error.response?.statusText || 'Connection failed'; // Try to get response body let responseBody = ''; if (error.response?.data) { if (typeof error.response.data === 'string') { responseBody = error.response.data; } else { responseBody = JSON.stringify(error.response.data); } } const diagnosticMessage = `API Error: ${method} ${url} → ${status} ${statusText}${responseBody ? '\nResponse: ' + responseBody : ''}`; console.error(diagnosticMessage); // Re-throw with enhanced message error.message = diagnosticMessage; return Promise.reject(error); } ); export const proteinApi = { /** * Get available filter options */ async getFilters(): Promise { const response = await api.get('/protein/filters'); return response.data; }, /** * Get filtered protein data */ async getData(filters: FilterParams): Promise { const response = await api.post('/protein/data', filters); return response.data; }, /** * Get summary statistics */ async getSummary(params?: Partial): Promise { const response = await api.get('/protein/summary', { params }); return response.data; }, /** * Resolve chain metadata from RCSB web API * @param signal - AbortSignal for request cancellation */ async resolveChain( pdb_id: string, auth_asym_id: string, options?: { use_cache?: boolean; signal?: AbortSignal } ): Promise { const { use_cache = true, signal } = options || {}; try { const response = await api.get('/protein/chain/resolve', { params: { pdb_id, auth_asym_id, use_cache }, signal }); return response.data; } catch (error: any) { // Normalize 404 errors if (error.response?.status === 404) { const notFoundError: any = new Error('Chain not found'); notFoundError.code = 'NOT_FOUND'; notFoundError.status = 404; notFoundError.detail = error.response?.data?.detail || 'Chain not found in PDB'; throw notFoundError; } throw error; } }, /** * Batch resolve multiple chains */ async batchResolveChains(chains: Array<{ pdb_id: string; auth_asym_id: string }>, use_cache: boolean = true): Promise> { const response = await api.post>('/protein/chain/batch-resolve', chains, { params: { use_cache } }); return response.data; }, /** * Get ranked functional annotations for a chain from local TSV */ async getChainFunctions(pdb_id: string, auth_asym_id: string): Promise { const response = await api.get<{ functions: string[] }>('/protein/chain/functions', { params: { pdb_id, auth_asym_id }, }); return response.data.functions; }, };