import { useState, useEffect } from 'react' import { useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { useMutation, useQuery } from '@tanstack/react-query' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Progress } from '@/components/ui/progress' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { useToast } from '@/components/ui/toast' import { apiClientV2 } from '@/services/apiV2' import { Play, CheckCircle, FileText, AlertCircle, Clock, Activity, Loader2 } from 'lucide-react' import LayoutModelSelector from '@/components/LayoutModelSelector' import PreprocessingSettings from '@/components/PreprocessingSettings' import PreprocessingPreview from '@/components/PreprocessingPreview' import ProcessingTrackSelector from '@/components/ProcessingTrackSelector' import TaskNotFound from '@/components/TaskNotFound' import BatchProcessingPanel from '@/components/BatchProcessingPanel' import { useTaskValidation } from '@/hooks/useTaskValidation' import { useTaskStore, useProcessingState, useIsBatchMode } from '@/store/taskStore' import type { LayoutModel, ProcessingOptions, PreprocessingMode, PreprocessingConfig, ProcessingTrack } from '@/types/apiV2' /** * ProcessingPage - Main entry point * Routes to batch or single task processing based on state */ export default function ProcessingPage() { const isBatchMode = useIsBatchMode() // Route to appropriate component if (isBatchMode) { return } return } /** * SingleTaskProcessing - Original single task processing UI */ function SingleTaskProcessing() { const { t } = useTranslation() const navigate = useNavigate() const { toast } = useToast() // Use TaskStore for processing state management const { startProcessing, stopProcessing, updateTaskStatus } = useTaskStore() // processingState is available for future use (e.g., displaying global processing status) const _processingState = useProcessingState() void _processingState // Suppress unused variable warning // Use shared hook for task validation const { taskId, taskDetail, isLoading: isValidating, isNotFound, clearAndReset } = useTaskValidation({ refetchInterval: (query) => { const data = query.state.data if (!data) return 2000 if (data.status === 'completed' || data.status === 'failed') { return false } return 2000 }, }) // Layout model state (default to 'chinese' for best Chinese document support) const [layoutModel, setLayoutModel] = useState('chinese') // Preprocessing state const [preprocessingMode, setPreprocessingMode] = useState('auto') const [preprocessingConfig, setPreprocessingConfig] = useState({ contrast: 'clahe', contrast_strength: 1.0, sharpen: true, sharpen_strength: 1.0, binarize: false, remove_scan_artifacts: true, }) const [showPreview, setShowPreview] = useState(false) // Processing track override state (null = use system recommendation) const [forceTrack, setForceTrack] = useState(null) // Analyze document to determine if OCR is needed (only for pending tasks) const { data: documentAnalysis, isLoading: isAnalyzing } = useQuery({ queryKey: ['documentAnalysis', taskId], queryFn: () => apiClientV2.analyzeDocument(taskId!), enabled: !!taskId && taskDetail?.status === 'pending', staleTime: Infinity, // Cache indefinitely since document doesn't change }) // Determine if preprocessing options should be shown // Show OCR options when: // 1. User explicitly selected OCR track // 2. OR system recommends OCR/hybrid track (and user hasn't overridden to direct) // 3. OR still analyzing (show by default) const needsOcrTrack = forceTrack === 'ocr' || (forceTrack === null && ( documentAnalysis?.recommended_track === 'ocr' || documentAnalysis?.recommended_track === 'hybrid' || !documentAnalysis )) // Start OCR processing // NOTE: Simple OCR mode - using backend defaults for table/chart/formula recognition // Only layout_model and preprocessing options are configurable from frontend const processOCRMutation = useMutation({ mutationFn: () => { const options: ProcessingOptions = { use_dual_track: forceTrack === null, // Only use dual-track auto-detection if not forcing force_track: forceTrack || undefined, // Pass force_track if user selected one language: 'ch', layout_model: layoutModel, preprocessing_mode: preprocessingMode, preprocessing_config: preprocessingMode === 'manual' ? preprocessingConfig : undefined, // NOTE: table_detection, ocr_preset, ocr_config removed - using backend defaults } // Update TaskStore processing state startProcessing(forceTrack, options) return apiClientV2.startTask(taskId!, options) }, onSuccess: () => { // Update task status in cache if (taskId) { updateTaskStatus(taskId, 'processing', forceTrack || undefined) } toast({ title: t('processing.startProcessing'), description: t('processing.ocrStarted'), variant: 'success', }) }, onError: (error: any) => { // Stop processing state on error stopProcessing() toast({ title: t('errors.processingFailed'), description: error.response?.data?.detail || t('errors.networkError'), variant: 'destructive', }) }, }) // Handle task status changes - update store and redirect when completed useEffect(() => { if (taskDetail?.status === 'completed') { // Stop processing state and update cache stopProcessing() if (taskId) { updateTaskStatus(taskId, 'completed', taskDetail.processing_track) } setTimeout(() => { navigate('/tasks') }, 1000) } else if (taskDetail?.status === 'failed') { // Stop processing state on failure stopProcessing() if (taskId) { updateTaskStatus(taskId, 'failed') } } }, [taskDetail?.status, taskDetail?.processing_track, taskId, navigate, stopProcessing, updateTaskStatus]) const handleStartProcessing = () => { processOCRMutation.mutate() } const handleViewResults = () => { navigate('/tasks') } const getStatusBadge = (status: string) => { switch (status) { case 'completed': return {t('processing.completed')} case 'processing': return {t('processing.processing')} case 'failed': return {t('processing.failed')} default: return {t('processing.pending')} } } const getProgressPercentage = (status: string) => { switch (status) { case 'completed': return 100 case 'processing': return 50 case 'failed': return 100 default: return 0 } } // Show loading while validating task if (isValidating) { return (

{t('processing.loadingTask')}

) } // Show message when task was deleted if (isNotFound) { return } // Show helpful message when no task is selected if (!taskId) { return (
{t('processing.title')}

{t('processing.noBatchMessage')}

) } const isProcessing = taskDetail?.status === 'processing' const isCompleted = taskDetail?.status === 'completed' const isPending = !taskDetail || taskDetail.status === 'pending' return (
{/* Page Header */}

{t('processing.title')}

{t('taskDetail.taskId', { id: taskId })} {taskDetail?.filename && ` ยท ${taskDetail.filename}`}

{isCompleted && (
{t('processing.completed')}
)} {isProcessing && (
{t('processing.processing')}
)}
{/* Overall Progress */}
{t('processing.progress')}
{taskDetail && getStatusBadge(taskDetail.status)}
{/* Progress bar */}
{t('processing.status')} {taskDetail ? getProgressPercentage(taskDetail.status) : 0}%
{/* Task Info */} {taskDetail && (

{t('taskDetail.filename')}

{taskDetail.filename || t('common.unknownFile')}

{taskDetail.processing_time_ms && (

{t('taskDetail.processingTime')}

{(taskDetail.processing_time_ms / 1000).toFixed(2)}s

)}
)} {/* Error message */} {taskDetail?.error_message && (

{t('processing.failed')}

{taskDetail.error_message}

)} {/* Action buttons */} {(isPending || isCompleted) && (
{isPending && ( )} {isCompleted && ( )}
)}
{/* Task Details Card */} {taskDetail && (
{t('taskDetail.title')}
{t('taskDetail.taskStatus')} {getStatusBadge(taskDetail.status)}
{t('taskDetail.createdAt')} {new Date(taskDetail.created_at).toLocaleString('zh-TW')}
{taskDetail.updated_at && (
{t('taskDetail.lastUpdated')} {new Date(taskDetail.updated_at).toLocaleString('zh-TW')}
)} {taskDetail.completed_at && (
{t('taskDetail.completedAt')} {new Date(taskDetail.completed_at).toLocaleString('zh-TW')}
)}
)} {/* Processing Options (only show when task is pending) */} {isPending && (
{/* Document Analysis Loading */} {isAnalyzing && (
{t('processing.analyzingDocument')}
)} {/* Processing Track Selector - Always show after analysis */} {!isAnalyzing && ( )} {/* OCR Track Options - Only show when document needs OCR */} {needsOcrTrack && !isAnalyzing && ( <> {/* Layout Model Selection */} {/* Preprocessing Settings */} setShowPreview(!showPreview)} disabled={processOCRMutation.isPending} /> {/* Preprocessing Preview */} {showPreview && taskId && ( )} )}
)}
) }