import { useMemo, useState, useEffect } from 'react' import { useParams, useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { useQuery } from '@tanstack/react-query' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import PDFViewer from '@/components/PDFViewer' import { useToast } from '@/components/ui/toast' import { apiClientV2 } from '@/services/apiV2' import { useTaskStore } from '@/store/taskStore' import { FileText, Download, AlertCircle, Clock, Layers, Loader2, ArrowLeft, RefreshCw, FileSearch, Table2, Image, BarChart3, Languages, Globe, CheckCircle, Trash2 } from 'lucide-react' import type { ProcessingTrack, TranslationStatus, TranslationListItem } from '@/types/apiV2' import { Badge } from '@/components/ui/badge' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Progress } from '@/components/ui/progress' // Language options for translation const LANGUAGE_OPTIONS = [ { value: 'en', label: 'English' }, { value: 'ja', label: '日本語' }, { value: 'ko', label: '한국어' }, { value: 'zh-TW', label: '繁體中文' }, { value: 'zh-CN', label: '简体中文' }, { value: 'de', label: 'Deutsch' }, { value: 'fr', label: 'Français' }, { value: 'es', label: 'Español' }, { value: 'pt', label: 'Português' }, { value: 'it', label: 'Italiano' }, { value: 'ru', label: 'Русский' }, { value: 'vi', label: 'Tiếng Việt' }, { value: 'th', label: 'ไทย' }, ] export default function TaskDetailPage() { const { taskId } = useParams<{ taskId: string }>() const { t, i18n } = useTranslation() const navigate = useNavigate() const { toast } = useToast() // TaskStore for caching const { updateTaskCache } = useTaskStore() // Translation state const [targetLang, setTargetLang] = useState('en') const [isTranslating, setIsTranslating] = useState(false) const [translationStatus, setTranslationStatus] = useState(null) const [translationProgress, setTranslationProgress] = useState(0) // Get task details const { data: taskDetail, isLoading, refetch } = useQuery({ queryKey: ['taskDetail', taskId], queryFn: () => apiClientV2.getTask(taskId!), enabled: !!taskId, refetchInterval: (query) => { const data = query.state.data if (!data) return 2000 if (data.status === 'completed' || data.status === 'failed') { return false } return 2000 // Poll every 2 seconds for processing tasks }, }) // Sync task details to TaskStore cache useEffect(() => { if (taskDetail) { updateTaskCache(taskDetail) } }, [taskDetail, updateTaskCache]) // Get processing metadata for completed tasks const { data: processingMetadata } = useQuery({ queryKey: ['processingMetadata', taskId], queryFn: () => apiClientV2.getProcessingMetadata(taskId!), enabled: !!taskId && taskDetail?.status === 'completed', retry: false, }) // Get existing translations const { data: translationList, refetch: refetchTranslations } = useQuery({ queryKey: ['translations', taskId], queryFn: () => apiClientV2.listTranslations(taskId!), enabled: !!taskId && taskDetail?.status === 'completed', retry: false, }) // Check for in-progress translation on page load useEffect(() => { if (!taskId || !taskDetail || taskDetail.status !== 'completed') return const checkTranslationStatus = async () => { try { const status = await apiClientV2.getTranslationStatus(taskId) if (status.status === 'translating' || status.status === 'pending') { // Resume polling for in-progress translation setIsTranslating(true) setTranslationStatus(status.status) if (status.progress) { setTranslationProgress(status.progress.percentage) } if (status.target_lang) { setTargetLang(status.target_lang) } } } catch { // No active translation job - this is normal } } checkTranslationStatus() }, [taskId, taskDetail]) // Poll translation status when translating useEffect(() => { if (!isTranslating || !taskId) return const pollInterval = setInterval(async () => { try { const status = await apiClientV2.getTranslationStatus(taskId) setTranslationStatus(status.status) if (status.progress) { setTranslationProgress(status.progress.percentage) } if (status.status === 'completed') { setIsTranslating(false) setTranslationProgress(100) toast({ title: t('translation.translationComplete'), description: `${LANGUAGE_OPTIONS.find(l => l.value === targetLang)?.label || targetLang}`, variant: 'success', }) refetchTranslations() } else if (status.status === 'failed') { setIsTranslating(false) toast({ title: t('translation.translationFailed'), description: status.error_message || t('common.unknownError'), variant: 'destructive', }) } } catch (error) { console.error('Failed to poll translation status:', error) } }, 2000) return () => clearInterval(pollInterval) }, [isTranslating, taskId, targetLang, toast, refetchTranslations, t]) // Construct PDF URL for preview - memoize to prevent unnecessary reloads // Must be called unconditionally before any early returns (React hooks rule) const API_BASE_URL = (import.meta.env.VITE_API_BASE_URL || '').replace(/\/$/, '') const pdfUrl = useMemo(() => { return taskId ? `${API_BASE_URL}/api/v2/tasks/${taskId}/download/pdf` : '' }, [taskId, API_BASE_URL]) // Get auth token for PDF preview - memoize to prevent new object reference each render const pdfHttpHeaders = useMemo(() => { const authToken = localStorage.getItem('auth_token_v2') return authToken ? { Authorization: `Bearer ${authToken}` } : undefined }, []) const getTrackBadge = (track?: ProcessingTrack) => { if (!track) return null switch (track) { case 'direct': return {t('taskDetail.track.direct')} case 'ocr': return OCR case 'hybrid': return {t('taskDetail.track.hybrid')} default: return {t('taskDetail.track.auto')} } } const getTrackDescription = (track?: ProcessingTrack) => { switch (track) { case 'direct': return t('taskDetail.track.directDesc') case 'ocr': return 'PP-StructureV3 OCR' case 'hybrid': return t('taskDetail.track.hybridDesc') default: return 'OCR' } } const handleDownloadLayoutPDF = async () => { if (!taskId) return try { await apiClientV2.downloadPDF(taskId, 'layout') toast({ title: t('export.exportSuccess'), description: t('taskDetail.layoutPdf'), variant: 'success', }) } catch (error: any) { toast({ title: t('export.exportError'), description: error.response?.data?.detail || t('errors.networkError'), variant: 'destructive', }) } } const handleDownloadReflowPDF = async () => { if (!taskId) return try { await apiClientV2.downloadPDF(taskId, 'reflow') toast({ title: t('export.exportSuccess'), description: t('taskDetail.reflowPdf'), variant: 'success', }) } catch (error: any) { toast({ title: t('export.exportError'), description: error.response?.data?.detail || t('errors.networkError'), variant: 'destructive', }) } } const handleStartTranslation = async () => { if (!taskId || isTranslating) return try { setIsTranslating(true) setTranslationStatus('pending') setTranslationProgress(0) const response = await apiClientV2.startTranslation(taskId, { target_lang: targetLang, source_lang: 'auto', }) if (response.status === 'completed') { // Translation already exists setIsTranslating(false) setTranslationProgress(100) toast({ title: t('translation.translationExists'), description: response.message, variant: 'success', }) refetchTranslations() } else { setTranslationStatus(response.status) toast({ title: t('translation.translationStarted'), description: t('translation.translationStartedDesc'), }) } } catch (error: any) { setIsTranslating(false) setTranslationStatus(null) toast({ title: t('errors.startFailed'), description: error.response?.data?.detail || t('errors.networkError'), variant: 'destructive', }) } } const handleDeleteTranslation = async (lang: string) => { if (!taskId) return try { await apiClientV2.deleteTranslation(taskId, lang) toast({ title: t('translation.deleteSuccess'), description: t('translation.translationDeleted', { lang }), variant: 'success', }) refetchTranslations() } catch (error: any) { toast({ title: t('errors.deleteFailed'), description: error.response?.data?.detail || t('errors.networkError'), variant: 'destructive', }) } } const handleDownloadTranslatedPdf = async (lang: string, format: 'layout' | 'reflow' = 'reflow') => { if (!taskId) return try { await apiClientV2.downloadTranslatedPdf(taskId, lang, format) const formatLabel = format === 'layout' ? t('taskDetail.layoutPdf') : t('taskDetail.reflowPdf') toast({ title: t('common.downloadSuccess'), description: `${formatLabel} (${lang})`, variant: 'success', }) } catch (error: any) { toast({ title: t('common.downloadFailed'), description: error.response?.data?.detail || t('errors.networkError'), variant: 'destructive', }) } } const handleDownloadVisualization = async () => { if (!taskId) return try { await apiClientV2.downloadVisualization(taskId) toast({ title: t('common.downloadSuccess'), description: t('taskDetail.visualizationDownloaded'), variant: 'success', }) } catch (error: any) { toast({ title: t('common.downloadFailed'), description: error.response?.data?.detail || t('errors.networkError'), variant: 'destructive', }) } } const getStatusBadge = (status: string) => { switch (status) { case 'completed': return {t('taskDetail.status.completed')} case 'processing': return {t('taskDetail.status.processing')} case 'failed': return {t('taskDetail.status.failed')} default: return {t('taskDetail.status.pending')} } } const getTranslationStatusText = (status: TranslationStatus | null) => { switch (status) { case 'pending': return t('translation.status.preparing') case 'loading_model': return t('translation.status.loadingModel') case 'translating': return t('translation.status.translating') case 'completed': return t('translation.status.complete') case 'failed': return t('taskDetail.status.failed') default: return '' } } const formatDate = (dateStr: string) => { const date = new Date(dateStr) return date.toLocaleString(i18n.language === 'en' ? 'en-US' : 'zh-TW') } if (isLoading) { return (

{t('taskDetail.loadingTask')}

) } if (!taskDetail) { return (
{t('taskDetail.taskNotFound')}

{t('taskDetail.taskNotFoundDesc', { id: taskId })}

) } const isCompleted = taskDetail.status === 'completed' const isProcessing = taskDetail.status === 'processing' const isFailed = taskDetail.status === 'failed' return (
{/* Page Header */}

{t('taskDetail.title')}

{t('taskDetail.taskId', { id: taskId })}

{getStatusBadge(taskDetail.status)}
{/* Task Info Card */} {t('taskDetail.taskInfo')}

{t('taskDetail.filename')}

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

{t('taskDetail.createdAt')}

{formatDate(taskDetail.created_at)}

{taskDetail.completed_at && (

{t('taskDetail.completedAt')}

{formatDate(taskDetail.completed_at)}

)}

{t('taskDetail.taskStatus')}

{getStatusBadge(taskDetail.status)}
{(taskDetail.processing_track || processingMetadata?.processing_track) && (

{t('taskDetail.processingTrack')}

{getTrackBadge(taskDetail.processing_track || processingMetadata?.processing_track)} {getTrackDescription(taskDetail.processing_track || processingMetadata?.processing_track)}
)} {taskDetail.processing_time_ms && (

{t('taskDetail.processingTime')}

{(taskDetail.processing_time_ms / 1000).toFixed(2)} {t('common.seconds')}

)} {taskDetail.updated_at && (

{t('taskDetail.lastUpdated')}

{formatDate(taskDetail.updated_at)}

)}
{/* Download Options */} {isCompleted && ( {t('taskDetail.downloadResults')}
{/* Visualization download for OCR Track */} {taskDetail?.has_visualization && (
)}
)} {/* Translation Options */} {isCompleted && ( {t('translation.title')} {/* Start New Translation */}
{t('translation.targetLanguage')}
{/* Translation Progress */} {isTranslating && (
{getTranslationStatusText(translationStatus)} {translationProgress.toFixed(0)}%
)} {/* Existing Translations */} {translationList && translationList.translations.length > 0 && (

{t('translation.completedTranslations')}

{translationList.translations.map((item: TranslationListItem) => (
{LANGUAGE_OPTIONS.find(l => l.value === item.target_lang)?.label || item.target_lang} ({item.statistics.translated_elements} elements, {item.statistics.processing_time_seconds.toFixed(1)}s)
))}
)}

{t('translation.description')}

)} {/* Error Message */} {isFailed && taskDetail.error_message && ( {t('taskDetail.errorMessage')}

{taskDetail.error_message}

)} {/* Processing Status */} {isProcessing && (

{t('taskDetail.processingInProgress')}

{t('taskDetail.processingInProgressDesc')}

)} {/* Stats Grid (for completed tasks) */} {isCompleted && (

{t('taskDetail.processingTime')}

{processingMetadata?.processing_time_seconds?.toFixed(2) || (taskDetail.processing_time_ms ? (taskDetail.processing_time_ms / 1000).toFixed(2) : '0')}s

{t('taskDetail.stats.pageCount')}

{processingMetadata?.page_count || '-'}

{t('taskDetail.stats.textRegions')}

{processingMetadata?.total_text_regions || '-'}

{t('taskDetail.stats.tables')}

{processingMetadata?.total_tables || '-'}

{t('taskDetail.stats.images')}

{processingMetadata?.total_images || '-'}

{t('taskDetail.stats.avgConfidence')}

{processingMetadata?.average_confidence ? `${(processingMetadata.average_confidence * 100).toFixed(0)}%` : '-'}

)} {/* Result Preview */} {isCompleted && ( )}
) }