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:
@@ -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>
|
||||
|
||||
@@ -494,16 +494,20 @@ class ApiClientV2 {
|
||||
|
||||
/**
|
||||
* Download task result as PDF
|
||||
* @param taskId - The task ID
|
||||
* @param format - PDF format: 'layout' (default) or 'reflow'
|
||||
*/
|
||||
async downloadPDF(taskId: string): Promise<void> {
|
||||
async downloadPDF(taskId: string, format: 'layout' | 'reflow' = 'layout'): Promise<void> {
|
||||
const response = await this.client.get(`/tasks/${taskId}/download/pdf`, {
|
||||
params: format === 'reflow' ? { format: 'reflow' } : undefined,
|
||||
responseType: 'blob',
|
||||
})
|
||||
|
||||
const blob = new Blob([response.data], { type: 'application/pdf' })
|
||||
const link = document.createElement('a')
|
||||
link.href = window.URL.createObjectURL(blob)
|
||||
link.download = `${taskId}_result.pdf`
|
||||
const formatSuffix = format === 'reflow' ? '_reflow' : '_layout'
|
||||
link.download = `${taskId}${formatSuffix}.pdf`
|
||||
link.click()
|
||||
window.URL.revokeObjectURL(link.href)
|
||||
}
|
||||
@@ -688,18 +692,26 @@ class ApiClientV2 {
|
||||
}
|
||||
|
||||
/**
|
||||
* Download translated PDF with layout preservation
|
||||
* Download translated PDF
|
||||
* @param taskId - The task ID
|
||||
* @param lang - Target language code
|
||||
* @param format - PDF format: 'layout' or 'reflow' (default: 'reflow')
|
||||
*/
|
||||
async downloadTranslatedPdf(taskId: string, lang: string): Promise<void> {
|
||||
async downloadTranslatedPdf(
|
||||
taskId: string,
|
||||
lang: string,
|
||||
format: 'layout' | 'reflow' = 'reflow'
|
||||
): Promise<void> {
|
||||
const response = await this.client.post(`/translate/${taskId}/pdf`, null, {
|
||||
params: { lang },
|
||||
params: { lang, format },
|
||||
responseType: 'blob',
|
||||
})
|
||||
|
||||
const formatSuffix = format === 'layout' ? '_layout' : '_reflow'
|
||||
const blob = new Blob([response.data], { type: 'application/pdf' })
|
||||
const link = document.createElement('a')
|
||||
link.href = window.URL.createObjectURL(blob)
|
||||
link.download = `${taskId}_translated_${lang}.pdf`
|
||||
link.download = `${taskId}_translated_${lang}${formatSuffix}.pdf`
|
||||
link.click()
|
||||
window.URL.revokeObjectURL(link.href)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user