feat: refactor dual-track architecture (Phase 1-5)

## Backend Changes
- **Service Layer Refactoring**:
  - Add ProcessingOrchestrator for unified document processing
  - Add PDFTableRenderer for table rendering extraction
  - Add PDFFontManager for font management with CJK support
  - Add MemoryPolicyEngine (73% code reduction from MemoryGuard)

- **Bug Fixes**:
  - Fix Direct Track table row span calculation
  - Fix OCR Track image path handling
  - Add cell_boxes coordinate validation
  - Filter out small decorative images
  - Add covering image detection

## Frontend Changes
- **State Management**:
  - Add TaskStore for centralized task state management
  - Add localStorage persistence for recent tasks
  - Add processing state tracking

- **Type Consolidation**:
  - Merge shared types from api.ts to apiV2.ts
  - Update imports in authStore, uploadStore, ResultsTable, SettingsPage

- **Page Integration**:
  - Integrate TaskStore in ProcessingPage and TaskDetailPage
  - Update useTaskValidation hook with cache sync

## Testing
- Direct Track: edit.pdf (3 pages, 1.281s), edit3.pdf (2 pages, 0.203s)
- Cell boxes validation: 43 valid, 0 invalid
- Table merging: 12 merged cells verified

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
egg
2025-12-07 07:18:27 +08:00
parent 8265be1741
commit eff9b0bcd5
19 changed files with 3637 additions and 173 deletions

View File

@@ -1,6 +1,6 @@
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import type { User } from '@/types/api'
import type { User } from '@/types/apiV2'
interface AuthState {
user: User | null

View File

@@ -0,0 +1,234 @@
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import type { Task, TaskStatus, ProcessingTrack, ProcessingOptions } from '@/types/apiV2'
/**
* Processing state for tracking ongoing operations
*/
export interface ProcessingState {
isProcessing: boolean
startedAt: string | null
track: ProcessingTrack | null
options: ProcessingOptions | null
}
/**
* Cached task info for quick display without API calls
*/
export interface CachedTask {
taskId: string
filename: string | null
status: TaskStatus
updatedAt: string
processingTrack?: ProcessingTrack
}
/**
* Task Store State
* Centralized state management for task operations
*/
interface TaskState {
// Current active task
currentTaskId: string | null
// Processing state for current task
processingState: ProcessingState
// Recently accessed tasks cache (max 20)
recentTasks: CachedTask[]
// Actions
setCurrentTask: (taskId: string | null, filename?: string | null) => void
clearCurrentTask: () => void
// Processing state actions
startProcessing: (track: ProcessingTrack | null, options?: ProcessingOptions) => void
stopProcessing: () => void
// Cache management
updateTaskCache: (task: Task | CachedTask) => void
updateTaskStatus: (taskId: string, status: TaskStatus, track?: ProcessingTrack) => void
removeFromCache: (taskId: string) => void
clearCache: () => void
// Get cached task
getCachedTask: (taskId: string) => CachedTask | undefined
}
/**
* Maximum number of recent tasks to cache
*/
const MAX_RECENT_TASKS = 20
/**
* Task Store
* Manages task state with localStorage persistence
*/
export const useTaskStore = create<TaskState>()(
persist(
(set, get) => ({
// Initial state
currentTaskId: null,
processingState: {
isProcessing: false,
startedAt: null,
track: null,
options: null,
},
recentTasks: [],
// Set current task
setCurrentTask: (taskId, filename) => {
set({ currentTaskId: taskId })
// Add to cache if we have task info
if (taskId && filename !== undefined) {
const existing = get().recentTasks.find(t => t.taskId === taskId)
if (!existing) {
get().updateTaskCache({
taskId,
filename,
status: 'pending',
updatedAt: new Date().toISOString(),
})
}
}
},
// Clear current task
clearCurrentTask: () => {
set({
currentTaskId: null,
processingState: {
isProcessing: false,
startedAt: null,
track: null,
options: null,
},
})
},
// Start processing
startProcessing: (track, options) => {
set({
processingState: {
isProcessing: true,
startedAt: new Date().toISOString(),
track,
options: options || null,
},
})
// Update cache status
const currentTaskId = get().currentTaskId
if (currentTaskId) {
get().updateTaskStatus(currentTaskId, 'processing', track || undefined)
}
},
// Stop processing
stopProcessing: () => {
set((state) => ({
processingState: {
...state.processingState,
isProcessing: false,
},
}))
},
// Update task in cache
updateTaskCache: (task) => {
set((state) => {
const taskId = 'task_id' in task ? task.task_id : task.taskId
const cached: CachedTask = {
taskId,
filename: task.filename || null,
status: task.status,
updatedAt: new Date().toISOString(),
processingTrack: 'processing_track' in task ? task.processing_track : task.processingTrack,
}
// Remove existing entry if present
const filtered = state.recentTasks.filter(t => t.taskId !== taskId)
// Add to front and limit size
const updated = [cached, ...filtered].slice(0, MAX_RECENT_TASKS)
return { recentTasks: updated }
})
},
// Update task status in cache
updateTaskStatus: (taskId, status, track) => {
set((state) => {
const updated = state.recentTasks.map(t => {
if (t.taskId === taskId) {
return {
...t,
status,
processingTrack: track || t.processingTrack,
updatedAt: new Date().toISOString(),
}
}
return t
})
return { recentTasks: updated }
})
},
// Remove task from cache
removeFromCache: (taskId) => {
set((state) => ({
recentTasks: state.recentTasks.filter(t => t.taskId !== taskId),
// Also clear current task if it matches
currentTaskId: state.currentTaskId === taskId ? null : state.currentTaskId,
}))
},
// Clear all cached tasks
clearCache: () => {
set({
recentTasks: [],
currentTaskId: null,
processingState: {
isProcessing: false,
startedAt: null,
track: null,
options: null,
},
})
},
// Get cached task by ID
getCachedTask: (taskId) => {
return get().recentTasks.find(t => t.taskId === taskId)
},
}),
{
name: 'tool-ocr-task-store',
// Only persist essential state, not processing state
partialize: (state) => ({
currentTaskId: state.currentTaskId,
recentTasks: state.recentTasks,
}),
}
)
)
/**
* Helper hook to get current task from cache
*/
export function useCurrentTask() {
const currentTaskId = useTaskStore((state) => state.currentTaskId)
const recentTasks = useTaskStore((state) => state.recentTasks)
if (!currentTaskId) return null
return recentTasks.find(t => t.taskId === currentTaskId) || null
}
/**
* Helper hook for processing state
*/
export function useProcessingState() {
return useTaskStore((state) => state.processingState)
}

View File

@@ -1,6 +1,6 @@
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import type { FileInfo } from '@/types/api'
import type { FileInfo } from '@/types/apiV2'
interface UploadState {
batchId: number | null