import axios, { AxiosError } from 'axios' import type { AxiosInstance } from 'axios' import type { LoginRequest, LoginResponse, UploadResponse, ProcessRequest, ProcessResponse, BatchStatus, OCRResult, ExportRequest, ExportRule, CSSTemplate, TranslateRequest, TranslateResponse, ApiError, } from '@/types/api' /** * API Client Configuration * - In Docker: VITE_API_BASE_URL is empty string, use relative path * - In development: Use VITE_API_BASE_URL from .env or default to localhost:8000 */ const envApiBaseUrl = import.meta.env.VITE_API_BASE_URL const API_BASE_URL = envApiBaseUrl !== undefined ? envApiBaseUrl : 'http://localhost:8000' const API_VERSION = 'v2' class ApiClient { private client: AxiosInstance private token: string | null = null constructor() { this.client = axios.create({ baseURL: `${API_BASE_URL}/api/${API_VERSION}`, timeout: 30000, headers: { 'Content-Type': 'application/json', }, }) // Request interceptor to add auth token this.client.interceptors.request.use( (config) => { if (this.token) { config.headers.Authorization = `Bearer ${this.token}` } return config }, (error) => Promise.reject(error) ) // Response interceptor for error handling this.client.interceptors.response.use( (response) => response, (error: AxiosError) => { if (error.response?.status === 401) { // Token expired or invalid this.clearToken() window.location.href = '/login' } return Promise.reject(error) } ) // Load token from localStorage this.loadToken() } /** * Set authentication token */ setToken(token: string) { this.token = token localStorage.setItem('auth_token', token) } /** * Clear authentication token */ clearToken() { this.token = null localStorage.removeItem('auth_token') } /** * Load token from localStorage */ private loadToken() { const token = localStorage.getItem('auth_token') if (token) { this.token = token } } /** * Check if user is authenticated */ isAuthenticated(): boolean { return this.token !== null } // ==================== Authentication ==================== /** * Login */ async login(data: LoginRequest): Promise { const response = await this.client.post('/auth/login', { username: data.username, password: data.password, }) this.setToken(response.data.access_token) return response.data } /** * Logout */ logout() { this.clearToken() } // ==================== File Upload ==================== /** * Upload files */ async uploadFiles(files: File[]): Promise { const formData = new FormData() files.forEach((file) => { formData.append('files', file) }) const response = await this.client.post('/upload', formData, { headers: { 'Content-Type': 'multipart/form-data', }, }) return response.data } // ==================== OCR Processing ==================== /** * Process OCR */ async processOCR(data: ProcessRequest): Promise { const response = await this.client.post('/ocr/process', data) return response.data } /** * Get OCR result by file ID * Note: Backend uses file-level tracking, not task-level */ async getOCRResult(fileId: number): Promise { const response = await this.client.get(`/ocr/result/${fileId}`) return response.data } /** * Get batch status */ async getBatchStatus(batchId: number): Promise { const response = await this.client.get(`/batch/${batchId}/status`) return response.data } // ==================== Export ==================== /** * Export results */ async exportResults(data: ExportRequest): Promise { const response = await this.client.post('/export', data, { responseType: 'blob', }) return response.data } /** * Generate and download PDF */ async exportPDF(fileId: number, cssTemplate?: string): Promise { const params = cssTemplate ? { css_template: cssTemplate } : {} const response = await this.client.get(`/export/pdf/${fileId}`, { params, responseType: 'blob', }) return response.data } /** * Get export rules */ async getExportRules(): Promise { const response = await this.client.get('/export/rules') return response.data } /** * Create export rule */ async createExportRule(rule: Omit): Promise { const response = await this.client.post('/export/rules', rule) return response.data } /** * Update export rule */ async updateExportRule(ruleId: number, rule: Partial): Promise { const response = await this.client.put(`/export/rules/${ruleId}`, rule) return response.data } /** * Delete export rule */ async deleteExportRule(ruleId: number): Promise { await this.client.delete(`/export/rules/${ruleId}`) } /** * Get CSS templates */ async getCSSTemplates(): Promise { const response = await this.client.get('/export/css-templates') return response.data } // ==================== Translation (FUTURE FEATURE - STUB) ==================== /** * Translate document (STUB - Not yet implemented) * This is a placeholder for future translation functionality * @throws Will throw error with status 501 (Not Implemented) */ async translateDocument(data: TranslateRequest): Promise { // This endpoint is expected to return 501 Not Implemented until Phase 5 const response = await this.client.post('/translate/document', data) return response.data } /** * Get translation configs (NOT IMPLEMENTED) * This endpoint does not exist on backend - configs will be part of Phase 5 * @deprecated Backend endpoint does not exist - will return 404 */ // async getTranslationConfigs(): Promise { // const response = await this.client.get('/translate/configs') // return response.data // } /** * Create translation config (NOT IMPLEMENTED) * This endpoint does not exist on backend - configs will be part of Phase 5 * @deprecated Backend endpoint does not exist - will return 404 */ // async createTranslationConfig( // config: Omit // ): Promise { // const response = await this.client.post('/translate/configs', config) // return response.data // } } // Export singleton instance export const apiClient = new ApiClient()