feat: add translated PDF format selection (layout/reflow)

- Add generate_translated_layout_pdf() method for layout-preserving translated PDFs
- Add generate_translated_pdf() method for reflow translated PDFs
- Update translate router to accept format parameter (layout/reflow)
- Update frontend with dropdown to select translated PDF format
- Fix reflow PDF table cell extraction from content dict
- Add embedded images handling in reflow PDF tables
- Archive improve-translated-text-fitting openspec proposal

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
egg
2025-12-03 10:10:28 +08:00
parent 0dcea4a7e7
commit 08adf3d01d
15 changed files with 1384 additions and 1222 deletions

View File

@@ -178,13 +178,31 @@ export default function TaskDetailPage() {
}
}
const handleDownloadPDF = async () => {
const handleDownloadLayoutPDF = async () => {
if (!taskId) return
try {
await apiClientV2.downloadPDF(taskId)
await apiClientV2.downloadPDF(taskId, 'layout')
toast({
title: t('export.exportSuccess'),
description: 'PDF 已下載',
description: '版面 PDF 已下載',
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: '流式 PDF 已下載',
variant: 'success',
})
} catch (error: any) {
@@ -328,13 +346,14 @@ export default function TaskDetailPage() {
}
}
const handleDownloadTranslatedPdf = async (lang: string) => {
const handleDownloadTranslatedPdf = async (lang: string, format: 'layout' | 'reflow' = 'reflow') => {
if (!taskId) return
try {
await apiClientV2.downloadTranslatedPdf(taskId, lang)
await apiClientV2.downloadTranslatedPdf(taskId, lang, format)
const formatLabel = format === 'layout' ? '版面' : '流式'
toast({
title: '下載成功',
description: `翻譯 PDF (${lang}) 已下載`,
description: `翻譯 ${formatLabel} PDF (${lang}) 已下載`,
variant: 'success',
})
} catch (error: any) {
@@ -513,7 +532,7 @@ export default function TaskDetailPage() {
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
<Button onClick={handleDownloadJSON} variant="outline" className="gap-2 h-20 flex-col">
<FileJson className="w-8 h-8" />
<span>JSON</span>
@@ -526,9 +545,13 @@ export default function TaskDetailPage() {
<FileText className="w-8 h-8" />
<span>Markdown</span>
</Button>
<Button onClick={handleDownloadPDF} className="gap-2 h-20 flex-col">
<Button onClick={handleDownloadLayoutPDF} className="gap-2 h-20 flex-col">
<Download className="w-8 h-8" />
<span>PDF</span>
<span> PDF</span>
</Button>
<Button onClick={handleDownloadReflowPDF} variant="outline" className="gap-2 h-20 flex-col">
<Download className="w-8 h-8" />
<span> PDF</span>
</Button>
</div>
</CardContent>
@@ -542,7 +565,6 @@ export default function TaskDetailPage() {
<CardTitle className="flex items-center gap-2">
<Languages className="w-5 h-5" />
<Badge variant="secondary" className="ml-2">MADLAD-400</Badge>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
@@ -624,15 +646,22 @@ export default function TaskDetailPage() {
<Download className="w-3 h-3" />
JSON
</Button>
<Button
variant="default"
size="sm"
onClick={() => handleDownloadTranslatedPdf(item.target_lang)}
className="gap-1"
<Select
onValueChange={(format: 'layout' | 'reflow') =>
handleDownloadTranslatedPdf(item.target_lang, format)
}
>
<FileOutput className="w-3 h-3" />
PDF
</Button>
<SelectTrigger className="w-[100px] h-8">
<div className="flex items-center gap-1">
<FileOutput className="w-3 h-3" />
<span className="text-xs">PDF</span>
</div>
</SelectTrigger>
<SelectContent>
<SelectItem value="layout"> PDF</SelectItem>
<SelectItem value="reflow"> PDF</SelectItem>
</SelectContent>
</Select>
<Button
variant="ghost"
size="sm"
@@ -649,7 +678,7 @@ export default function TaskDetailPage() {
)}
<p className="text-xs text-muted-foreground">
使 MADLAD-400-3B 450+ 10-30
使
</p>
</CardContent>
</Card>