Files
OCR/frontend/src/pages/UploadPage.tsx
egg cfe65158a3 feat: enable document orientation detection for scanned PDFs
- Enable PP-StructureV3's use_doc_orientation_classify feature
- Detect rotation angle from doc_preprocessor_res.angle
- Swap page dimensions (width <-> height) for 90°/270° rotations
- Output PDF now correctly displays landscape-scanned content

Also includes:
- Archive completed openspec proposals
- Add simplify-frontend-ocr-config proposal (pending)
- Code cleanup and frontend simplification

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-11 17:13:46 +08:00

276 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { useMutation } from '@tanstack/react-query'
import FileUpload from '@/components/FileUpload'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { useToast } from '@/components/ui/toast'
import { useUploadStore } from '@/store/uploadStore'
import { apiClientV2 } from '@/services/apiV2'
import { FileText, X, Upload, Trash2, CheckCircle2, ArrowRight } from 'lucide-react'
export default function UploadPage() {
const { t } = useTranslation()
const navigate = useNavigate()
const { toast } = useToast()
const [selectedFiles, setSelectedFiles] = useState<File[]>([])
const { setBatchId, setUploadProgress } = useUploadStore()
const uploadMutation = useMutation({
mutationFn: async (files: File[]) => {
// Upload files one by one and collect task IDs
const tasks = []
for (const file of files) {
const result = await apiClientV2.uploadFile(file)
tasks.push(result)
}
return tasks
},
onSuccess: (tasks) => {
// Use the first task_id as the current batch identifier
// Note: Type assertion needed - store expects number but API returns string UUID
if (tasks.length > 0) {
setBatchId(tasks[0].task_id as unknown as number)
}
toast({
title: t('upload.uploadSuccess'),
description: `成功上傳 ${tasks.length} 個檔案`,
variant: 'success',
})
navigate('/processing')
},
onError: (error: any) => {
toast({
title: t('upload.uploadError'),
description: error.response?.data?.detail || t('errors.networkError'),
variant: 'destructive',
})
},
})
const handleFilesSelected = (files: File[]) => {
setSelectedFiles((prev) => [...prev, ...files])
}
const handleRemoveFile = (index: number) => {
setSelectedFiles((prev) => prev.filter((_, i) => i !== index))
}
const handleClearAll = () => {
setSelectedFiles([])
setUploadProgress(0)
}
const handleUpload = () => {
if (selectedFiles.length === 0) {
toast({
title: t('errors.validationError'),
description: '請選擇至少一個檔案',
variant: 'destructive',
})
return
}
uploadMutation.mutate(selectedFiles)
}
const formatFileSize = (bytes: number) => {
if (bytes === 0) return '0 Bytes'
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
}
const getFileIcon = (filename: string) => {
const ext = filename.split('.').pop()?.toLowerCase()
const colors = {
pdf: 'text-red-500',
doc: 'text-blue-500',
docx: 'text-blue-500',
ppt: 'text-orange-500',
pptx: 'text-orange-500',
jpg: 'text-green-500',
jpeg: 'text-green-500',
png: 'text-green-500',
}
return colors[ext as keyof typeof colors] || 'text-gray-500'
}
const totalSize = selectedFiles.reduce((acc, file) => acc + file.size, 0)
return (
<div className="space-y-6">
{/* Page Header */}
<div className="page-header">
<h1 className="page-title">{t('upload.title')}</h1>
<p className="text-muted-foreground">
OCR PDF Office
</p>
</div>
{/* Step Indicator */}
<div className="flex items-center gap-4">
<div className="flex items-center gap-3">
<div className={`flex items-center justify-center w-10 h-10 rounded-full ${
selectedFiles.length === 0 ? 'bg-primary text-white' : 'bg-success text-white'
}`}>
{selectedFiles.length === 0 ? '1' : <CheckCircle2 className="w-5 h-5" />}
</div>
<div>
<div className="text-sm font-medium text-foreground"></div>
<div className="text-xs text-muted-foreground"></div>
</div>
</div>
<ArrowRight className="w-5 h-5 text-muted-foreground" />
<div className="flex items-center gap-3">
<div className={`flex items-center justify-center w-10 h-10 rounded-full ${
selectedFiles.length > 0 ? 'bg-primary text-white' : 'bg-muted text-muted-foreground'
}`}>
2
</div>
<div>
<div className={`text-sm font-medium ${
selectedFiles.length > 0 ? 'text-foreground' : 'text-muted-foreground'
}`}></div>
<div className="text-xs text-muted-foreground"></div>
</div>
</div>
<ArrowRight className="w-5 h-5 text-muted-foreground" />
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-muted text-muted-foreground">
3
</div>
<div>
<div className="text-sm font-medium text-muted-foreground"></div>
<div className="text-xs text-muted-foreground"></div>
</div>
</div>
</div>
{/* Upload Area */}
<div className="section">
<FileUpload
onFilesSelected={handleFilesSelected}
disabled={uploadMutation.isPending}
/>
</div>
{/* Selected Files Section */}
{selectedFiles.length > 0 && (
<div className="space-y-4">
{/* Summary Card */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 bg-primary/10 rounded-lg">
<FileText className="w-5 h-5 text-primary" />
</div>
<div>
<CardTitle className="text-lg">
{t('upload.selectedFiles')}
</CardTitle>
<p className="text-sm text-muted-foreground mt-0.5">
{selectedFiles.length} {formatFileSize(totalSize)}
</p>
</div>
</div>
<Button
variant="outline"
size="sm"
onClick={handleClearAll}
disabled={uploadMutation.isPending}
className="gap-2"
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</CardHeader>
<CardContent>
{/* Files Table */}
<div className="space-y-2">
{selectedFiles.map((file, index) => (
<div
key={index}
className="group flex items-center gap-4 p-3 rounded-lg border border-border hover:bg-muted/50 transition-colors"
>
{/* File icon */}
<div className={`p-2 rounded-lg bg-muted/50 ${getFileIcon(file.name)}`}>
<FileText className="w-5 h-5" />
</div>
{/* File info */}
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground truncate">
{file.name}
</p>
<p className="text-xs text-muted-foreground">
{formatFileSize(file.size)} · {file.type || '未知類型'}
</p>
</div>
{/* Status badge */}
<div className="status-badge-success">
</div>
{/* Remove button */}
<button
onClick={() => handleRemoveFile(index)}
disabled={uploadMutation.isPending}
className="p-2 rounded-lg text-muted-foreground hover:bg-destructive/10 hover:text-destructive transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title="移除檔案"
>
<X className="w-4 h-4" />
</button>
</div>
))}
</div>
</CardContent>
</Card>
{/* Action Bar */}
<div className="flex items-center justify-between p-4 bg-card rounded-xl border border-border">
<div className="text-sm text-muted-foreground">
</div>
<div className="flex gap-3">
<Button
variant="outline"
onClick={handleClearAll}
disabled={uploadMutation.isPending}
>
</Button>
<Button
onClick={handleUpload}
disabled={uploadMutation.isPending}
className="gap-2"
>
{uploadMutation.isPending ? (
<>
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
{t('upload.uploading')}
</>
) : (
<>
<Upload className="w-4 h-4" />
</>
)}
</Button>
</div>
</div>
</div>
)}
</div>
)
}