feat: add batch processing for multiple file uploads

- Add BatchState management in taskStore with progress tracking
- Implement batch processing service with concurrency control
  - Direct Track: max 5 parallel tasks
  - OCR Track: sequential processing (GPU VRAM limit)
- Refactor ProcessingPage to support batch mode with BatchProcessingPanel
- Update UploadPage to initialize batch state for multi-file uploads
- Add i18n translations for batch processing (zh-TW, en-US)

🤖 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-12 17:05:16 +08:00
parent d5bc311757
commit d20751d56b
11 changed files with 1469 additions and 5 deletions

View File

@@ -1,6 +1,6 @@
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import type { Task, TaskStatus, ProcessingTrack, ProcessingOptions } from '@/types/apiV2'
import type { Task, TaskStatus, ProcessingTrack, ProcessingOptions, DocumentAnalysisResponse } from '@/types/apiV2'
/**
* Processing state for tracking ongoing operations
@@ -12,6 +12,76 @@ export interface ProcessingState {
options: ProcessingOptions | null
}
// ==================== Batch Processing Types ====================
/**
* Processing strategy for batch operations
*/
export type BatchStrategy = 'auto' | 'force_ocr' | 'force_direct'
/**
* Individual task state within a batch
*/
export interface BatchTaskState {
taskId: string
filename: string | null
status: TaskStatus
track: ProcessingTrack | null
recommendedTrack: ProcessingTrack | null
analysisResult: DocumentAnalysisResponse | null
error: string | null
startedAt: string | null
completedAt: string | null
}
/**
* Batch progress tracking
*/
export interface BatchProgress {
total: number
pending: number
analyzing: number
processing: number
completed: number
failed: number
}
/**
* Batch processing options (unified settings for all tasks)
*/
export interface BatchProcessingOptions {
strategy: BatchStrategy
layoutModel: 'chinese' | 'default' | 'cdla'
preprocessingMode: 'auto' | 'manual' | 'disabled'
language: string
}
/**
* Batch state for managing multiple tasks
*/
export interface BatchState {
// Whether batch mode is active
isActive: boolean
// All task IDs in this batch
taskIds: string[]
// State for each task
taskStates: Record<string, BatchTaskState>
// Progress summary
progress: BatchProgress
// Unified processing options
processingOptions: BatchProcessingOptions
// Batch processing status
isProcessing: boolean
isAnalyzing: boolean
startedAt: string | null
completedAt: string | null
}
/**
* Cached task info for quick display without API calls
*/
@@ -23,6 +93,43 @@ export interface CachedTask {
processingTrack?: ProcessingTrack
}
/**
* Default batch processing options
*/
const defaultBatchProcessingOptions: BatchProcessingOptions = {
strategy: 'auto',
layoutModel: 'chinese',
preprocessingMode: 'auto',
language: 'ch',
}
/**
* Default batch progress
*/
const defaultBatchProgress: BatchProgress = {
total: 0,
pending: 0,
analyzing: 0,
processing: 0,
completed: 0,
failed: 0,
}
/**
* Default batch state
*/
const defaultBatchState: BatchState = {
isActive: false,
taskIds: [],
taskStates: {},
progress: defaultBatchProgress,
processingOptions: defaultBatchProcessingOptions,
isProcessing: false,
isAnalyzing: false,
startedAt: null,
completedAt: null,
}
/**
* Task Store State
* Centralized state management for task operations
@@ -37,6 +144,9 @@ interface TaskState {
// Recently accessed tasks cache (max 20)
recentTasks: CachedTask[]
// Batch processing state
batchState: BatchState
// Actions
setCurrentTask: (taskId: string | null, filename?: string | null) => void
clearCurrentTask: () => void
@@ -53,6 +163,16 @@ interface TaskState {
// Get cached task
getCachedTask: (taskId: string) => CachedTask | undefined
// Batch processing actions
initBatch: (tasks: Array<{ taskId: string; filename: string | null }>) => void
clearBatch: () => void
setBatchOptions: (options: Partial<BatchProcessingOptions>) => void
updateBatchTaskState: (taskId: string, updates: Partial<BatchTaskState>) => void
setBatchAnalyzing: (isAnalyzing: boolean) => void
startBatchProcessing: () => void
stopBatchProcessing: () => void
recalculateBatchProgress: () => void
}
/**
@@ -76,6 +196,7 @@ export const useTaskStore = create<TaskState>()(
options: null,
},
recentTasks: [],
batchState: { ...defaultBatchState },
// Set current task
setCurrentTask: (taskId, filename) => {
@@ -207,6 +328,179 @@ export const useTaskStore = create<TaskState>()(
getCachedTask: (taskId) => {
return get().recentTasks.find(t => t.taskId === taskId)
},
// ==================== Batch Processing Actions ====================
// Initialize batch with uploaded tasks
initBatch: (tasks) => {
const taskIds = tasks.map(t => t.taskId)
const taskStates: Record<string, BatchTaskState> = {}
for (const task of tasks) {
taskStates[task.taskId] = {
taskId: task.taskId,
filename: task.filename,
status: 'pending',
track: null,
recommendedTrack: null,
analysisResult: null,
error: null,
startedAt: null,
completedAt: null,
}
}
set({
batchState: {
isActive: true,
taskIds,
taskStates,
progress: {
total: tasks.length,
pending: tasks.length,
analyzing: 0,
processing: 0,
completed: 0,
failed: 0,
},
processingOptions: { ...defaultBatchProcessingOptions },
isProcessing: false,
isAnalyzing: false,
startedAt: null,
completedAt: null,
},
})
},
// Clear batch state
clearBatch: () => {
set({ batchState: { ...defaultBatchState } })
},
// Update batch processing options
setBatchOptions: (options) => {
set((state) => ({
batchState: {
...state.batchState,
processingOptions: {
...state.batchState.processingOptions,
...options,
},
},
}))
},
// Update individual task state within batch
updateBatchTaskState: (taskId, updates) => {
set((state) => {
const currentTask = state.batchState.taskStates[taskId]
if (!currentTask) return state
const updatedTaskStates = {
...state.batchState.taskStates,
[taskId]: {
...currentTask,
...updates,
},
}
return {
batchState: {
...state.batchState,
taskStates: updatedTaskStates,
},
}
})
// Recalculate progress after update
get().recalculateBatchProgress()
},
// Set batch analyzing state
setBatchAnalyzing: (isAnalyzing) => {
set((state) => ({
batchState: {
...state.batchState,
isAnalyzing,
},
}))
},
// Start batch processing
startBatchProcessing: () => {
set((state) => ({
batchState: {
...state.batchState,
isProcessing: true,
startedAt: new Date().toISOString(),
completedAt: null,
},
}))
},
// Stop batch processing
stopBatchProcessing: () => {
set((state) => ({
batchState: {
...state.batchState,
isProcessing: false,
completedAt: new Date().toISOString(),
},
}))
},
// Recalculate batch progress from task states
recalculateBatchProgress: () => {
set((state) => {
const { taskStates, taskIds } = state.batchState
let pending = 0
let analyzing = 0
let processing = 0
let completed = 0
let failed = 0
for (const taskId of taskIds) {
const taskState = taskStates[taskId]
if (!taskState) continue
switch (taskState.status) {
case 'pending':
// Check if we're analyzing (has recommendedTrack means analyzed)
if (taskState.recommendedTrack !== null) {
pending++
} else if (state.batchState.isAnalyzing) {
analyzing++
} else {
pending++
}
break
case 'processing':
processing++
break
case 'completed':
completed++
break
case 'failed':
failed++
break
}
}
return {
batchState: {
...state.batchState,
progress: {
total: taskIds.length,
pending,
analyzing,
processing,
completed,
failed,
},
},
}
})
},
}),
{
name: 'tool-ocr-task-store',
@@ -236,3 +530,31 @@ export function useCurrentTask() {
export function useProcessingState() {
return useTaskStore((state) => state.processingState)
}
/**
* Helper hook for batch state
*/
export function useBatchState() {
return useTaskStore((state) => state.batchState)
}
/**
* Helper hook for batch progress
*/
export function useBatchProgress() {
return useTaskStore((state) => state.batchState.progress)
}
/**
* Helper hook for batch processing options
*/
export function useBatchOptions() {
return useTaskStore((state) => state.batchState.processingOptions)
}
/**
* Helper hook to check if batch mode is active
*/
export function useIsBatchMode() {
return useTaskStore((state) => state.batchState.isActive && state.batchState.taskIds.length > 1)
}