feat: optimize task file generation and add visualization download

Backend changes:
- Disable PP-Structure debug file generation by default
- Separate raw_ocr_regions.json generation from debug flag (critical file)
- Add visualization folder download endpoint as ZIP
- Add has_visualization field to TaskDetailResponse
- Stop generating Markdown files
- Save translated PDFs to task folder with caching

Frontend changes:
- Replace JSON/MD download buttons with PDF buttons in TaskHistoryPage
- Add visualization download button in TaskDetailPage
- Fix Processing page task switching issue (reset isNotFound)

Archives two OpenSpec proposals:
- optimize-task-files-and-visualization
- simplify-frontend-add-billing

🤖 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 19:11:50 +08:00
parent 65abd51d60
commit efa7e4175c
14 changed files with 534 additions and 97 deletions

View File

@@ -34,6 +34,11 @@ export function useTaskValidation(options?: {
const [isNotFound, setIsNotFound] = useState(false)
// Reset isNotFound when taskId changes (new upload)
useEffect(() => {
setIsNotFound(false)
}, [taskId])
const { data: taskDetail, isLoading, error, isFetching } = useQuery({
queryKey: ['taskDetail', taskId],
queryFn: () => apiClientV2.getTask(taskId!),

View File

@@ -300,6 +300,24 @@ export default function TaskDetailPage() {
}
}
const handleDownloadVisualization = async () => {
if (!taskId) return
try {
await apiClientV2.downloadVisualization(taskId)
toast({
title: '下載成功',
description: '辨識結果圖片已下載',
variant: 'success',
})
} catch (error: any) {
toast({
title: '下載失敗',
description: error.response?.data?.detail || t('errors.networkError'),
variant: 'destructive',
})
}
}
const getStatusBadge = (status: string) => {
switch (status) {
case 'completed':
@@ -477,6 +495,19 @@ export default function TaskDetailPage() {
<span> PDF</span>
</Button>
</div>
{/* Visualization download for OCR Track */}
{taskDetail?.has_visualization && (
<div className="mt-3 pt-3 border-t">
<Button
onClick={handleDownloadVisualization}
variant="secondary"
className="w-full gap-2"
>
<Image className="w-4 h-4" />
(ZIP)
</Button>
</div>
)}
</CardContent>
</Card>
)}

View File

@@ -155,17 +155,11 @@ export default function TaskHistoryPage() {
}
// Download handlers
const handleDownload = async (taskId: string, format: 'json' | 'markdown' | 'pdf') => {
const handleDownloadPDF = async (taskId: string, format: 'layout' | 'reflow') => {
try {
if (format === 'json') {
await apiClientV2.downloadJSON(taskId)
} else if (format === 'markdown') {
await apiClientV2.downloadMarkdown(taskId)
} else if (format === 'pdf') {
await apiClientV2.downloadPDF(taskId)
}
await apiClientV2.downloadPDF(taskId, format)
} catch (err: any) {
alert(err.response?.data?.detail || `下載 ${format.toUpperCase()} 檔案失敗`)
alert(err.response?.data?.detail || `下載 PDF 檔案失敗`)
}
}
@@ -509,39 +503,24 @@ export default function TaskHistoryPage() {
{/* Download actions for completed tasks */}
{task.status === 'completed' && (
<>
{task.result_json_path && (
<Button
variant="outline"
size="sm"
onClick={() => handleDownload(task.task_id, 'json')}
title="下載 JSON"
>
<Download className="w-3 h-3 mr-1" />
JSON
</Button>
)}
{task.result_markdown_path && (
<Button
variant="outline"
size="sm"
onClick={() => handleDownload(task.task_id, 'markdown')}
title="下載 Markdown"
>
<Download className="w-3 h-3 mr-1" />
MD
</Button>
)}
{task.result_pdf_path && (
<Button
variant="outline"
size="sm"
onClick={() => handleDownload(task.task_id, 'pdf')}
title="下載 PDF"
>
<Download className="w-3 h-3 mr-1" />
PDF
</Button>
)}
<Button
variant="outline"
size="sm"
onClick={() => handleDownloadPDF(task.task_id, 'layout')}
title="下載版面 PDF"
>
<Download className="w-3 h-3 mr-1" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleDownloadPDF(task.task_id, 'reflow')}
title="下載流式 PDF"
>
<Download className="w-3 h-3 mr-1" />
</Button>
<Button
variant="outline"
size="sm"

View File

@@ -527,6 +527,22 @@ class ApiClientV2 {
window.URL.revokeObjectURL(link.href)
}
/**
* Download visualization images as ZIP (OCR Track only)
*/
async downloadVisualization(taskId: string): Promise<void> {
const response = await this.client.get(`/tasks/${taskId}/download/visualization`, {
responseType: 'blob',
})
const blob = new Blob([response.data], { type: 'application/zip' })
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = `${taskId}_visualization.zip`
link.click()
window.URL.revokeObjectURL(link.href)
}
// ==================== Preprocessing Preview APIs ====================
/**

View File

@@ -197,6 +197,7 @@ export interface TaskFile {
export interface TaskDetail extends Task {
files: TaskFile[]
has_visualization?: boolean
}
export interface TaskListResponse {