Files
OCR/frontend/src/services/api.ts
egg ad5c8be0a3 fix: add V2 file upload endpoint and update frontend to v2 API
Add missing file upload functionality to V2 API that was removed
during V1 to V2 migration. Update frontend to use v2 API endpoints.

Backend changes:
- Add /api/v2/upload endpoint in main.py for file uploads
- Import necessary dependencies (UploadFile, hashlib, TaskFile)
- Upload endpoint creates task, saves file, and returns task info
- Add UploadResponse schema to task.py schemas
- Update tasks router imports for consistency

Frontend changes:
- Update API_VERSION from 'v1' to 'v2' in api.ts
- Update UploadResponse type to match V2 API response format
  (task_id instead of batch_id, single file instead of array)

This fixes the 404 error when uploading files from the frontend.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-16 19:13:22 +08:00

272 lines
6.9 KiB
TypeScript

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<ApiError>) => {
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<LoginResponse> {
const response = await this.client.post<LoginResponse>('/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<UploadResponse> {
const formData = new FormData()
files.forEach((file) => {
formData.append('files', file)
})
const response = await this.client.post<UploadResponse>('/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
return response.data
}
// ==================== OCR Processing ====================
/**
* Process OCR
*/
async processOCR(data: ProcessRequest): Promise<ProcessResponse> {
const response = await this.client.post<ProcessResponse>('/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<OCRResult> {
const response = await this.client.get<OCRResult>(`/ocr/result/${fileId}`)
return response.data
}
/**
* Get batch status
*/
async getBatchStatus(batchId: number): Promise<BatchStatus> {
const response = await this.client.get<BatchStatus>(`/batch/${batchId}/status`)
return response.data
}
// ==================== Export ====================
/**
* Export results
*/
async exportResults(data: ExportRequest): Promise<Blob> {
const response = await this.client.post('/export', data, {
responseType: 'blob',
})
return response.data
}
/**
* Generate and download PDF
*/
async exportPDF(fileId: number, cssTemplate?: string): Promise<Blob> {
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<ExportRule[]> {
const response = await this.client.get<ExportRule[]>('/export/rules')
return response.data
}
/**
* Create export rule
*/
async createExportRule(rule: Omit<ExportRule, 'id' | 'created_at'>): Promise<ExportRule> {
const response = await this.client.post<ExportRule>('/export/rules', rule)
return response.data
}
/**
* Update export rule
*/
async updateExportRule(ruleId: number, rule: Partial<ExportRule>): Promise<ExportRule> {
const response = await this.client.put<ExportRule>(`/export/rules/${ruleId}`, rule)
return response.data
}
/**
* Delete export rule
*/
async deleteExportRule(ruleId: number): Promise<void> {
await this.client.delete(`/export/rules/${ruleId}`)
}
/**
* Get CSS templates
*/
async getCSSTemplates(): Promise<CSSTemplate[]> {
const response = await this.client.get<CSSTemplate[]>('/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<TranslateResponse> {
// This endpoint is expected to return 501 Not Implemented until Phase 5
const response = await this.client.post<TranslateResponse>('/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<TranslationConfig[]> {
// const response = await this.client.get<TranslationConfig[]>('/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<TranslationConfig, 'id' | 'created_at'>
// ): Promise<TranslationConfig> {
// const response = await this.client.post<TranslationConfig>('/translate/configs', config)
// return response.data
// }
}
// Export singleton instance
export const apiClient = new ApiClient()