fix: migrate ProcessingPage from V1 batch API to V2 task API

Changes:
- Replace apiClient with apiClientV2 for task queries
- Update from batch status polling to task detail polling
- Change from batch_id to task_id (UUID string)
- Simplify UI to show single task instead of batch with multiple files
- Update redirect from /results to /tasks page
- Add task details card with timestamps
- Add error message display for failed tasks
- Calculate progress based on task status (pending: 0%, processing: 50%, completed/failed: 100%)

Fixes:
- 404 error: GET /api/v2/batch/{id}/status (endpoint no longer exists in V2)
- Continuous polling to non-existent batch endpoint

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
egg
2025-11-16 19:31:32 +08:00
parent 439458c7fe
commit ff566c3af4

View File

@@ -8,18 +8,21 @@ import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { useToast } from '@/components/ui/toast' import { useToast } from '@/components/ui/toast'
import { useUploadStore } from '@/store/uploadStore' import { useUploadStore } from '@/store/uploadStore'
import { apiClient } from '@/services/api' import { apiClientV2 } from '@/services/apiV2'
import { Play, CheckCircle, FileText, AlertCircle, Clock, Activity, Loader2 } from 'lucide-react' import { Play, CheckCircle, FileText, AlertCircle, Clock, Activity, Loader2 } from 'lucide-react'
export default function ProcessingPage() { export default function ProcessingPage() {
const { t } = useTranslation() const { t } = useTranslation()
const navigate = useNavigate() const navigate = useNavigate()
const { toast } = useToast() const { toast } = useToast()
const { batchId, files } = useUploadStore() const { batchId } = useUploadStore()
// In V2, batchId is actually a task_id (string)
const taskId = batchId ? String(batchId) : null
// Start OCR processing // Start OCR processing
const processOCRMutation = useMutation({ const processOCRMutation = useMutation({
mutationFn: () => apiClient.processOCR({ batch_id: batchId! }), mutationFn: () => apiClientV2.startTask(taskId!),
onSuccess: () => { onSuccess: () => {
toast({ toast({
title: '開始處理', title: '開始處理',
@@ -36,16 +39,16 @@ export default function ProcessingPage() {
}, },
}) })
// Poll batch status // Poll task status
const { data: batchStatus } = useQuery({ const { data: taskDetail } = useQuery({
queryKey: ['batchStatus', batchId], queryKey: ['taskDetail', taskId],
queryFn: () => apiClient.getBatchStatus(batchId!), queryFn: () => apiClientV2.getTask(taskId!),
enabled: !!batchId, enabled: !!taskId,
refetchInterval: (query) => { refetchInterval: (query) => {
const data = query.state.data const data = query.state.data
if (!data) return 2000 if (!data) return 2000
// Stop polling if completed or failed // Stop polling if completed or failed
if (data.batch.status === 'completed' || data.batch.status === 'failed') { if (data.status === 'completed' || data.status === 'failed') {
return false return false
} }
return 2000 // Poll every 2 seconds return 2000 // Poll every 2 seconds
@@ -54,19 +57,19 @@ export default function ProcessingPage() {
// Auto-redirect when completed // Auto-redirect when completed
useEffect(() => { useEffect(() => {
if (batchStatus?.batch.status === 'completed') { if (taskDetail?.status === 'completed') {
setTimeout(() => { setTimeout(() => {
navigate('/results') navigate('/tasks')
}, 1000) }, 1000)
} }
}, [batchStatus?.batch.status, navigate]) }, [taskDetail?.status, navigate])
const handleStartProcessing = () => { const handleStartProcessing = () => {
processOCRMutation.mutate() processOCRMutation.mutate()
} }
const handleViewResults = () => { const handleViewResults = () => {
navigate('/results') navigate('/tasks')
} }
const getStatusBadge = (status: string) => { const getStatusBadge = (status: string) => {
@@ -82,8 +85,21 @@ export default function ProcessingPage() {
} }
} }
// Show helpful message when no batch is selected const getProgressPercentage = (status: string) => {
if (!batchId) { switch (status) {
case 'completed':
return 100
case 'processing':
return 50
case 'failed':
return 100
default:
return 0
}
}
// Show helpful message when no task is selected
if (!taskId) {
return ( return (
<div className="flex items-center justify-center min-h-[60vh]"> <div className="flex items-center justify-center min-h-[60vh]">
<Card className="max-w-md text-center"> <Card className="max-w-md text-center">
@@ -97,7 +113,7 @@ export default function ProcessingPage() {
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<p className="text-muted-foreground"> <p className="text-muted-foreground">
{t('processing.noBatchMessage', { defaultValue: '尚未選擇任何批次。請先上傳檔案以建立批次。' })} {t('processing.noBatchMessage', { defaultValue: '尚未選擇任何任務。請先上傳檔案以建立任務。' })}
</p> </p>
<Button <Button
onClick={() => navigate('/upload')} onClick={() => navigate('/upload')}
@@ -111,9 +127,9 @@ export default function ProcessingPage() {
) )
} }
const isProcessing = batchStatus?.batch.status === 'processing' const isProcessing = taskDetail?.status === 'processing'
const isCompleted = batchStatus?.batch.status === 'completed' const isCompleted = taskDetail?.status === 'completed'
const isPending = !batchStatus || batchStatus.batch.status === 'pending' const isPending = !taskDetail || taskDetail.status === 'pending'
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@@ -123,7 +139,8 @@ export default function ProcessingPage() {
<div> <div>
<h1 className="page-title">{t('processing.title')}</h1> <h1 className="page-title">{t('processing.title')}</h1>
<p className="text-muted-foreground mt-1"> <p className="text-muted-foreground mt-1">
ID: <span className="font-mono text-primary">{batchId}</span> · {files.length} ID: <span className="font-mono text-primary">{taskId}</span>
{taskDetail?.filename && ` · ${taskDetail.filename}`}
</p> </p>
</div> </div>
<div> <div>
@@ -153,7 +170,7 @@ export default function ProcessingPage() {
</div> </div>
<CardTitle>{t('processing.progress')}</CardTitle> <CardTitle>{t('processing.progress')}</CardTitle>
</div> </div>
{batchStatus && getStatusBadge(batchStatus.batch.status)} {taskDetail && getStatusBadge(taskDetail.status)}
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="space-y-6"> <CardContent className="space-y-6">
@@ -162,63 +179,58 @@ export default function ProcessingPage() {
<div className="flex justify-between text-sm mb-3"> <div className="flex justify-between text-sm mb-3">
<span className="text-muted-foreground font-medium">{t('processing.status')}</span> <span className="text-muted-foreground font-medium">{t('processing.status')}</span>
<span className="font-bold text-xl text-primary"> <span className="font-bold text-xl text-primary">
{batchStatus?.batch.progress_percentage || 0}% {taskDetail ? getProgressPercentage(taskDetail.status) : 0}%
</span> </span>
</div> </div>
<Progress value={batchStatus?.batch.progress_percentage || 0} max={100} className="h-4" /> <Progress
value={taskDetail ? getProgressPercentage(taskDetail.status) : 0}
max={100}
className="h-4"
/>
</div> </div>
{/* Stats */} {/* Task Info */}
{batchStatus && ( {taskDetail && (
<div className="grid grid-cols-1 md:grid-cols-4 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="p-4 bg-muted/30 rounded-lg border border-border">
<div className="flex items-center gap-3">
<div className="p-2 bg-success/10 rounded-lg">
<CheckCircle className="w-5 h-5 text-success" />
</div>
<div>
<p className="text-xs text-muted-foreground mb-0.5"></p>
<p className="text-2xl font-bold text-foreground">
{batchStatus.files.filter((f) => f.status === 'completed').length}
</p>
</div>
</div>
</div>
<div className="p-4 bg-muted/30 rounded-lg border border-border"> <div className="p-4 bg-muted/30 rounded-lg border border-border">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="p-2 bg-primary/10 rounded-lg"> <div className="p-2 bg-primary/10 rounded-lg">
<Loader2 className="w-5 h-5 text-primary" /> <FileText className="w-5 h-5 text-primary" />
</div> </div>
<div> <div>
<p className="text-xs text-muted-foreground mb-0.5"></p> <p className="text-xs text-muted-foreground mb-0.5"></p>
<p className="text-2xl font-bold text-foreground"> <p className="text-sm font-medium text-foreground truncate">
{batchStatus.files.filter((f) => f.status === 'processing').length} {taskDetail.filename || '未知檔案'}
</p> </p>
</div> </div>
</div> </div>
</div> </div>
<div className="p-4 bg-muted/30 rounded-lg border border-border"> {taskDetail.processing_time_ms && (
<div className="flex items-center gap-3"> <div className="p-4 bg-muted/30 rounded-lg border border-border">
<div className="p-2 bg-destructive/10 rounded-lg"> <div className="flex items-center gap-3">
<AlertCircle className="w-5 h-5 text-destructive" /> <div className="p-2 bg-success/10 rounded-lg">
</div> <Clock className="w-5 h-5 text-success" />
<div> </div>
<p className="text-xs text-muted-foreground mb-0.5"></p> <div>
<p className="text-2xl font-bold text-foreground"> <p className="text-xs text-muted-foreground mb-0.5"></p>
{batchStatus.files.filter((f) => f.status === 'failed').length} <p className="text-sm font-medium text-foreground">
</p> {(taskDetail.processing_time_ms / 1000).toFixed(2)}s
</p>
</div>
</div> </div>
</div> </div>
</div> )}
<div className="p-4 bg-muted/30 rounded-lg border border-border"> </div>
<div className="flex items-center gap-3"> )}
<div className="p-2 bg-muted rounded-lg">
<FileText className="w-5 h-5 text-muted-foreground" /> {/* Error message */}
</div> {taskDetail?.error_message && (
<div> <div className="p-4 bg-destructive/10 rounded-lg border border-destructive/20">
<p className="text-xs text-muted-foreground mb-0.5"></p> <div className="flex items-start gap-3">
<p className="text-2xl font-bold text-foreground">{batchStatus.files.length}</p> <AlertCircle className="w-5 h-5 text-destructive flex-shrink-0 mt-0.5" />
</div> <div>
<p className="text-sm font-medium text-destructive mb-1"></p>
<p className="text-sm text-destructive/80">{taskDetail.error_message}</p>
</div> </div>
</div> </div>
</div> </div>
@@ -255,7 +267,7 @@ export default function ProcessingPage() {
size="lg" size="lg"
> >
<CheckCircle className="w-4 h-4" /> <CheckCircle className="w-4 h-4" />
{t('common.next')}
</Button> </Button>
)} )}
</div> </div>
@@ -263,64 +275,45 @@ export default function ProcessingPage() {
</CardContent> </CardContent>
</Card> </Card>
{/* File List */} {/* Task Details Card */}
{batchStatus && ( {taskDetail && (
<Card> <Card>
<CardHeader> <CardHeader>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="p-2 bg-primary/10 rounded-lg"> <div className="p-2 bg-primary/10 rounded-lg">
<FileText className="w-5 h-5 text-primary" /> <FileText className="w-5 h-5 text-primary" />
</div> </div>
<CardTitle></CardTitle> <CardTitle></CardTitle>
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-2"> <div className="space-y-3">
{batchStatus.files.map((file) => ( <div className="flex justify-between py-2 border-b border-border">
<div <span className="text-sm text-muted-foreground"></span>
key={file.id} {getStatusBadge(taskDetail.status)}
className="flex items-center justify-between p-4 rounded-lg border border-border hover:bg-muted/50 transition-colors" </div>
> <div className="flex justify-between py-2 border-b border-border">
<div className="flex items-center gap-4 flex-1 min-w-0"> <span className="text-sm text-muted-foreground"></span>
<div className={`p-2 rounded-lg ${ <span className="text-sm font-medium">
file.status === 'completed' ? 'bg-success/10' : {new Date(taskDetail.created_at).toLocaleString('zh-TW')}
file.status === 'processing' ? 'bg-primary/10' : </span>
file.status === 'failed' ? 'bg-destructive/10' : </div>
'bg-muted' {taskDetail.updated_at && (
}`}> <div className="flex justify-between py-2 border-b border-border">
{file.status === 'completed' ? ( <span className="text-sm text-muted-foreground"></span>
<CheckCircle className="w-5 h-5 text-success" /> <span className="text-sm font-medium">
) : file.status === 'processing' ? ( {new Date(taskDetail.updated_at).toLocaleString('zh-TW')}
<Loader2 className="w-5 h-5 text-primary animate-spin" /> </span>
) : file.status === 'failed' ? (
<AlertCircle className="w-5 h-5 text-destructive" />
) : (
<FileText className="w-5 h-5 text-muted-foreground" />
)}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground truncate">
{file.filename}
</p>
<div className="flex items-center gap-3 mt-1">
{file.processing_time && (
<p className="text-xs text-muted-foreground flex items-center gap-1">
<Clock className="w-3 h-3" />
: {file.processing_time.toFixed(2)}s
</p>
)}
{file.error && (
<p className="text-xs text-destructive flex items-center gap-1">
<AlertCircle className="w-3 h-3" />
{file.error}
</p>
)}
</div>
</div>
</div>
{getStatusBadge(file.status)}
</div> </div>
))} )}
{taskDetail.completed_at && (
<div className="flex justify-between py-2 border-b border-border">
<span className="text-sm text-muted-foreground"></span>
<span className="text-sm font-medium">
{new Date(taskDetail.completed_at).toLocaleString('zh-TW')}
</span>
</div>
)}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>