refactor: remove unused code and migrate legacy API

Backend cleanup:
- Remove ocr_service_original.py (legacy OCR service, replaced by ocr_service.py)
- Remove preprocessor.py (unused, functionality absorbed by layout_preprocessing_service.py)
- Remove pdf_font_manager.py (unused, never referenced by any service)

Frontend cleanup:
- Remove MarkdownPreview.tsx (unused component)
- Remove ResultsTable.tsx (unused, replaced by TaskHistoryPage)
- Remove services/api.ts (legacy API client, migrated to apiV2)
- Remove types/api.ts (legacy types, migrated to apiV2.ts)

API migration:
- Add export rules CRUD methods to apiClientV2
- Update SettingsPage.tsx to use apiClientV2
- Update Layout.tsx to use only apiClientV2 for logout

This reduces ~1,500 lines of redundant code and unifies the API client.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
egg
2025-12-11 12:03:09 +08:00
parent 940a406dce
commit 5d962ca97c
10 changed files with 40 additions and 1958 deletions

View File

@@ -1,7 +1,6 @@
import { Outlet, NavLink, useNavigate } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { useAuthStore } from '@/store/authStore'
import { apiClient } from '@/services/api'
import { apiClientV2 } from '@/services/apiV2'
import {
Upload,
@@ -29,12 +28,7 @@ export default function Layout() {
const handleLogout = async () => {
try {
// Use V2 API if authenticated with V2
if (apiClientV2.isAuthenticated()) {
await apiClientV2.logout()
} else {
apiClient.logout()
}
await apiClientV2.logout()
} catch (error) {
console.error('Logout error:', error)
} finally {

View File

@@ -1,26 +0,0 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
interface MarkdownPreviewProps {
title?: string
content: string
className?: string
}
export default function MarkdownPreview({ title, content, className }: MarkdownPreviewProps) {
return (
<Card className={className}>
{title && (
<CardHeader>
<CardTitle>{title}</CardTitle>
</CardHeader>
)}
<CardContent>
<div className="prose prose-sm max-w-none dark:prose-invert">
<pre className="whitespace-pre-wrap break-words bg-muted p-4 rounded-md overflow-auto max-h-[600px]">
{content}
</pre>
</div>
</CardContent>
</Card>
)
}

View File

@@ -1,90 +0,0 @@
import { useTranslation } from 'react-i18next'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import type { FileResult } from '@/types/apiV2'
interface ResultsTableProps {
files: FileResult[]
onViewResult?: (fileId: number) => void
onDownloadPDF?: (fileId: number) => void
}
export default function ResultsTable({ files, onViewResult, onDownloadPDF }: ResultsTableProps) {
const { t } = useTranslation()
const getStatusBadge = (status: FileResult['status']) => {
switch (status) {
case 'completed':
return <Badge variant="success">{t('processing.completed')}</Badge>
case 'processing':
return <Badge variant="default">{t('processing.processing')}</Badge>
case 'failed':
return <Badge variant="destructive">{t('processing.failed')}</Badge>
default:
return <Badge variant="secondary">{t('processing.pending')}</Badge>
}
}
const formatTime = (seconds?: number) => {
if (!seconds) return 'N/A'
return `${seconds.toFixed(2)}s`
}
return (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>{t('results.filename')}</TableHead>
<TableHead>{t('results.status')}</TableHead>
<TableHead>{t('results.processingTime')}</TableHead>
<TableHead className="text-right">{t('results.actions')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{files.length === 0 ? (
<TableRow>
<TableCell colSpan={4} className="text-center text-muted-foreground">
{t('results.noResults')}
</TableCell>
</TableRow>
) : (
files.map((file) => (
<TableRow key={file.id}>
<TableCell className="font-medium">{file.filename}</TableCell>
<TableCell>{getStatusBadge(file.status)}</TableCell>
<TableCell>{formatTime(file.processing_time)}</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
{file.status === 'completed' && (
<>
<Button
variant="outline"
size="sm"
onClick={() => onViewResult?.(file.id)}
>
{t('results.viewMarkdown')}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => onDownloadPDF?.(file.id)}
>
{t('results.downloadPDF')}
</Button>
</>
)}
{file.status === 'failed' && file.error && (
<span className="text-sm text-destructive">{file.error}</span>
)}
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
)
}

View File

@@ -4,7 +4,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { useToast } from '@/components/ui/toast'
import { apiClient } from '@/services/api'
import { apiClientV2 } from '@/services/apiV2'
import type { ExportRule } from '@/types/apiV2'
export default function SettingsPage() {
@@ -25,12 +25,12 @@ export default function SettingsPage() {
// Fetch export rules
const { data: exportRules, isLoading } = useQuery({
queryKey: ['exportRules'],
queryFn: () => apiClient.getExportRules(),
queryFn: () => apiClientV2.getExportRules(),
})
// Create rule mutation
const createRuleMutation = useMutation({
mutationFn: (rule: any) => apiClient.createExportRule(rule),
mutationFn: (rule: any) => apiClientV2.createExportRule(rule),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['exportRules'] })
setIsCreating(false)
@@ -53,7 +53,7 @@ export default function SettingsPage() {
// Update rule mutation
const updateRuleMutation = useMutation({
mutationFn: ({ ruleId, rule }: { ruleId: number; rule: any }) =>
apiClient.updateExportRule(ruleId, rule),
apiClientV2.updateExportRule(ruleId, rule),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['exportRules'] })
setEditingRule(null)
@@ -75,7 +75,7 @@ export default function SettingsPage() {
// Delete rule mutation
const deleteRuleMutation = useMutation({
mutationFn: (ruleId: number) => apiClient.deleteExportRule(ruleId),
mutationFn: (ruleId: number) => apiClientV2.deleteExportRule(ruleId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['exportRules'] })
toast({

View File

@@ -1,271 +0,0 @@
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()

View File

@@ -38,6 +38,7 @@ import type {
TranslationStatusResponse,
TranslationListResponse,
TranslationResult,
ExportRule,
} from '@/types/apiV2'
/**
@@ -713,6 +714,39 @@ class ApiClientV2 {
link.click()
window.URL.revokeObjectURL(link.href)
}
// ==================== Export Rules APIs ====================
/**
* 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}`)
}
}
// Export singleton instance

View File

@@ -1,182 +0,0 @@
/**
* API Type Definitions
* Based on backend OpenAPI specification
*/
// Authentication
export interface LoginRequest {
username: string
password: string
}
export interface LoginResponse {
access_token: string
token_type: string
expires_in: number // Token expiration time in seconds
}
export interface User {
id: number
username: string
email?: string
displayName?: string | null
}
// File Upload (V2 API)
export interface UploadResponse {
task_id: string
filename: string
file_size: number
file_type: string
status: 'pending' | 'processing' | 'completed' | 'failed'
}
export interface FileInfo {
id: number
filename: string
file_size: number
file_format: string // Changed from 'format' to match backend
status: 'pending' | 'processing' | 'completed' | 'failed'
}
// OCR Processing
export interface ProcessRequest {
batch_id: number
lang?: string
detect_layout?: boolean // Changed from confidence_threshold to match backend
}
export interface ProcessResponse {
message: string // Added to match backend
batch_id: number
total_files: number // Added to match backend
status: string
// Removed task_id - backend uses batch-level tracking instead
}
export interface TaskStatus {
task_id: string
status: 'pending' | 'processing' | 'completed' | 'failed'
progress_percentage: number
current_file?: string
files_processed: number
total_files: number
error?: string
}
export interface BatchStatus {
batch: {
id: number
status: 'pending' | 'processing' | 'completed' | 'failed'
progress_percentage: number
created_at: string
completed_at?: string
}
files: FileResult[]
}
export interface FileResult {
id: number
filename: string
status: 'pending' | 'processing' | 'completed' | 'failed'
processing_time?: number
error?: string
}
// OCR Results
export interface OCRResult {
file_id: number
filename: string
status: string
markdown_content: string
json_data: OCRJsonData
confidence: number
processing_time: number
}
export interface OCRJsonData {
total_text_regions: number
average_confidence: number
text_blocks: TextBlock[]
layout_info?: LayoutInfo
}
export interface TextBlock {
text: string
confidence: number
bbox: [number, number, number, number]
position: number
}
export interface LayoutInfo {
tables_detected: number
images_detected: number
structure: string
}
// Export
export interface ExportRequest {
batch_id: number
format: 'txt' | 'json' | 'excel' | 'markdown' | 'pdf'
rule_id?: number
options?: ExportOptions
}
export interface ExportOptions {
confidence_threshold?: number
include_metadata?: boolean
filename_pattern?: string
css_template?: string
}
export interface ExportRule {
id: number
rule_name: string
config_json: Record<string, any>
css_template?: string
created_at: string
}
export interface CSSTemplate {
name: string
description: string
// filename is not returned by backend - use name as identifier
}
// Translation (FUTURE FEATURE)
export interface TranslateRequest {
file_id: number
source_lang: string
target_lang: string
engine_type?: 'argos' | 'ernie' | 'google'
}
export interface TranslateResponse {
task_id: string
file_id: number
status: 'pending' | 'processing' | 'completed' | 'failed'
translated_content?: string
}
export interface TranslationConfig {
id: number
source_lang: string
target_lang: string
engine_type: 'argos' | 'ernie' | 'google'
engine_config: Record<string, any>
created_at: string
}
// API Response
export interface ApiResponse<T = any> {
success: boolean
data?: T
error?: string
message?: string
}
// Error Response
export interface ApiError {
detail: string
status_code: number
}