Skip to content

Commit c2425bc

Browse files
committed
feat(ResearchReport): use print-js to export PDF
1 parent 6e541d5 commit c2425bc

File tree

9 files changed

+112
-305
lines changed

9 files changed

+112
-305
lines changed

components/DeepResearch/NodeDetail.vue

-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
:description="node.error"
2323
color="error"
2424
variant="soft"
25-
:duration="8000"
2625
:actions="[
2726
{
2827
label: $t('webBrowsing.retry'),

components/ResearchReport.vue

+49-93
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script setup lang="ts">
2+
import printJS from 'print-js'
23
import { marked } from 'marked'
34
import { writeFinalReport } from '~/lib/deep-research'
4-
import jsPDF from 'jspdf'
55
import {
66
feedbackInjectionKey,
77
formInjectionKey,
@@ -33,7 +33,6 @@
3333
loadingExportPdf.value ||
3434
loadingExportMarkdown.value,
3535
)
36-
let pdf: jsPDF | undefined
3736
3837
async function generateReport() {
3938
loading.value = true
@@ -55,7 +54,7 @@
5554
} else if (chunk.type === 'text-delta') {
5655
reportContent.value += chunk.textDelta
5756
} else if (chunk.type === 'error') {
58-
error.value = t('researchReport.error', [
57+
error.value = t('researchReport.generateFailed', [
5958
chunk.error instanceof Error
6059
? chunk.error.message
6160
: String(chunk.error),
@@ -67,102 +66,54 @@
6766
)}\n\n${visitedUrls.map((url) => `- ${url}`).join('\n')}`
6867
} catch (e: any) {
6968
console.error(`Generate report failed`, e)
70-
error.value = t('researchReport.error', [e.message])
69+
error.value = t('researchReport.generateFailed', [e.message])
7170
} finally {
7271
loading.value = false
7372
}
7473
}
7574
7675
async function exportToPdf() {
77-
const element = document.getElementById('report-content')
78-
if (!element) return
79-
80-
loadingExportPdf.value = true
81-
82-
try {
83-
// 创建 PDF 实例
84-
if (!pdf) {
85-
pdf = new jsPDF({
86-
orientation: 'portrait',
87-
unit: 'mm',
88-
format: 'a4',
89-
})
90-
}
91-
92-
// Load Chinese font
93-
if (locale.value === 'zh') {
94-
try {
95-
if (!pdf.getFontList().SourceHanSans?.length) {
96-
toast.add({
97-
title: t('researchReport.downloadingFonts'),
98-
duration: 5000,
99-
color: 'info',
100-
})
101-
// Wait for 100ms to avoid toast being blocked by PDF generation
102-
await new Promise((resolve) => setTimeout(resolve, 100))
103-
const fontUrl = '/fonts/SourceHanSansCN-VF.ttf'
104-
pdf.addFont(fontUrl, 'SourceHanSans', 'normal')
105-
pdf.setFont('SourceHanSans')
106-
}
107-
} catch (e: any) {
108-
toast.add({
109-
title: t('researchReport.downloadFontFailed'),
110-
description: e.message,
111-
duration: 8000,
112-
color: 'error',
113-
})
114-
console.warn(
115-
'Failed to load Chinese font, fallback to default font:',
116-
e,
117-
)
118-
}
119-
}
120-
121-
// 设置字体大小和行高
122-
const fontSize = 10.5
123-
const lineHeight = 1.5
124-
pdf.setFontSize(fontSize)
125-
126-
// 设置页面边距(单位:mm)
127-
const margin = {
128-
top: 20,
129-
right: 20,
130-
bottom: 20,
131-
left: 20,
132-
}
133-
134-
// 获取纯文本内容
135-
const content = element.innerText
136-
137-
// 计算可用宽度(mm)
138-
const pageWidth = pdf.internal.pageSize.getWidth()
139-
const maxWidth = pageWidth - margin.left - margin.right
140-
141-
// 分割文本为行
142-
const lines = pdf.splitTextToSize(content, maxWidth)
143-
144-
// 计算当前位置
145-
let y = margin.top
146-
147-
// 逐行添加文本
148-
for (const line of lines) {
149-
// 检查是否需要新页
150-
if (y > pdf.internal.pageSize.getHeight() - margin.bottom) {
151-
pdf.addPage()
152-
y = margin.top
153-
}
154-
155-
// 添加文本
156-
pdf.text(line, margin.left, y)
157-
y += fontSize * lineHeight
158-
}
159-
160-
pdf.save('research-report.pdf')
161-
} catch (error) {
162-
console.error('Export to PDF failed:', error)
163-
} finally {
76+
// Change the title back
77+
const cleanup = () => {
78+
useHead({
79+
title: 'Deep Research Web UI',
80+
})
16481
loadingExportPdf.value = false
16582
}
83+
loadingExportPdf.value = true
84+
// Temporarily change the document title, which will be used as the filename
85+
useHead({
86+
title: `Deep Research Report - ${form.value.query ?? 'Untitled'}`,
87+
})
88+
// Wait after title is changed
89+
await new Promise((r) => setTimeout(r, 100))
90+
91+
printJS({
92+
printable: reportHtml.value,
93+
type: 'raw-html',
94+
showModal: true,
95+
onIncompatibleBrowser() {
96+
toast.add({
97+
title: t('researchReport.incompatibleBrowser'),
98+
description: t('researchReport.incompatibleBrowserDescription'),
99+
duration: 10_000,
100+
})
101+
cleanup()
102+
},
103+
onError(error, xmlHttpRequest) {
104+
console.error(`[Export PDF] failed:`, error, xmlHttpRequest)
105+
toast.add({
106+
title: t('researchReport.exportFailed'),
107+
description: error instanceof Error ? error.message : String(error),
108+
duration: 10_000,
109+
})
110+
cleanup()
111+
},
112+
onPrintDialogClose() {
113+
cleanup()
114+
},
115+
})
116+
return
166117
}
167118
168119
async function exportToMarkdown() {
@@ -210,7 +161,13 @@
210161
</div>
211162
</template>
212163

213-
<div v-if="error" class="text-red-500">{{ error }}</div>
164+
<UAlert
165+
v-if="error"
166+
:title="$t('researchReport.exportFailed')"
167+
:description="error"
168+
color="error"
169+
variant="soft"
170+
/>
214171

215172
<div class="flex mb-4 justify-end">
216173
<UButton
@@ -246,7 +203,6 @@
246203

247204
<div
248205
v-if="reportContent"
249-
id="report-content"
250206
class="prose prose-sm max-w-none break-words p-6 bg-gray-50 dark:bg-gray-800 dark:prose-invert dark:text-white rounded-lg shadow"
251207
v-html="reportHtml"
252208
/>

composables/useWebSearch.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ export const useWebSearch = (): WebSearchFunction => {
5454
return async (q: string, o: WebSearchOptions) => {
5555
const results = await tvly.search(q, {
5656
...o,
57-
searchDepth: config.webSearch.tavilyAdvancedSearch ? 'advanced' : 'basic',
57+
searchDepth: config.webSearch.tavilyAdvancedSearch
58+
? 'advanced'
59+
: 'basic',
5860
topic: config.webSearch.tavilySearchTopic,
5961
})
6062
return results.results

i18n/en.json

+6-5
Original file line numberDiff line numberDiff line change
@@ -92,15 +92,16 @@
9292
},
9393
"researchReport": {
9494
"title": "4. Research Report",
95-
"exportPdf": "Export PDF",
95+
"exportPdf": "Print (PDF)",
9696
"exportMarkdown": "Export Markdown",
9797
"sources": "Sources",
9898
"waiting": "Waiting for report...",
9999
"generating": "Generating report...",
100-
"error": "Generate report failed: {0}",
101-
"downloadingFonts": "Downloading necessary fonts, this may take some time...",
102-
"downloadFontFailed": "Download font failed",
103-
"regenerate": "Regenerate"
100+
"regenerate": "Regenerate",
101+
"incompatibleBrowser": "Incompatibile browser",
102+
"incompatibleBrowserDescription": "Your browser does not support this printing method. Please consider using a modern browser, or export the Markdown and manually convert it into PDF using other services, like https://md-to-pdf.fly.dev (Use at your own risk)",
103+
"exportFailed": "Export failed",
104+
"generateFailed": "Generate report failed: {0}"
104105
},
105106
"error": {
106107
"requestBlockedByCORS": "The current API provider may not allow cross-origin requests. Please try a different service provider or contact the provider for support."

i18n/nl.json

+6-5
Original file line numberDiff line numberDiff line change
@@ -92,15 +92,16 @@
9292
},
9393
"researchReport": {
9494
"title": "4. Onderzoeksrapport",
95-
"exportPdf": "PDF exporteren",
95+
"exportPdf": "Afdrukken (PDF)",
9696
"exportMarkdown": "Markdown exporteren",
9797
"sources": "Bronnen",
9898
"waiting": "Wachten op het rapport...",
9999
"generating": "Rapport genereren...",
100-
"error": "Rapport genereren mislukt: {0}",
101-
"downloadingFonts": "Het downloaden van de benodigde lettertypen kan enige tijd duren...",
102-
"downloadFontFailed": "Downloaden van lettertype mislukt",
103-
"regenerate": "Regenereren"
100+
"regenerate": "Regenereren",
101+
"generateFailed": "Rapport genereren mislukt: {0}",
102+
"exportFailed": "Export mislukt",
103+
"incompatibleBrowser": "Onverenigbaar browser",
104+
"incompatibleBrowserDescription": "Uw browser ondersteunt deze printmethode niet. Overweeg een moderne browser te gebruiken, of exporteer de Markdown en converteer deze handmatig naar PDF met behulp van andere diensten zoals https://md-to-pdf.fly.dev (Gebruik op eigen risico)"
104105
},
105106
"error": {
106107
"requestBlockedByCORS": "De huidige API-provider staat mogelijk geen cross-origin-verzoeken toe. Probeer een andere API-provider of neem contact op met de provider voor ondersteuning.."

i18n/zh.json

+6-5
Original file line numberDiff line numberDiff line change
@@ -92,15 +92,16 @@
9292
},
9393
"researchReport": {
9494
"title": "4. 研究报告",
95-
"exportPdf": "导出 PDF",
95+
"exportPdf": "打印 (PDF)",
9696
"exportMarkdown": "导出 Markdown",
9797
"sources": "来源",
9898
"waiting": "等待报告...",
9999
"generating": "生成报告中...",
100-
"error": "生成报告失败:{0}",
101-
"downloadingFonts": "正在下载必要字体,可能需要较长时间...",
102-
"downloadFontFailed": "下载字体失败",
103-
"regenerate": "重新生成"
100+
"regenerate": "重新生成",
101+
"generateFailed": "生成报告失败:{0}",
102+
"exportFailed": "导出失败",
103+
"incompatibleBrowserDescription": "您的浏览器不支持这种打印方式,请考虑改用更现代的浏览器,或者导出 Markdown 并手动使用其他服务(如 https://md-to-pdf.fly.dev)将其转换为 PDF(不承诺安全性)。",
104+
"incompatibleBrowser": "浏览器可能较旧"
104105
},
105106
"error": {
106107
"requestBlockedByCORS": "当前 API 服务可能不允许接口跨域,请换一个服务试试,或者向服务方反馈。"

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@
3131
"@vue-flow/core": "^1.42.1",
3232
"ai": "^4.1.41",
3333
"js-tiktoken": "^1.0.19",
34-
"jspdf": "^2.5.2",
3534
"marked": "^15.0.7",
3635
"nuxt": "^3.15.4",
3736
"p-limit": "^6.2.0",
3837
"pinia": "^3.0.1",
38+
"print-js": "^1.6.0",
3939
"semver": "^7.7.1",
4040
"tailwindcss": "^4.0.5",
4141
"vue": "latest",

0 commit comments

Comments
 (0)