Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"element-plus": "^2.10.2",
"file-saver": "^2.0.5",
"highlight.js": "^11.11.1",
"html-to-image": "^1.11.13",
"html2canvas": "^1.4.1",
"jspdf": "^3.0.1",
"katex": "^0.16.10",
Expand All @@ -45,6 +46,7 @@
"recorder-core": "^1.3.25011100",
"screenfull": "^6.0.2",
"sortablejs": "^1.15.6",
"svg2pdf.js": "^2.5.0",
"use-element-plus-theme": "^0.0.5",
"vite-plugin-html": "^3.2.2",
"vue": "^3.5.13",
Expand Down
146 changes: 146 additions & 0 deletions ui/src/components/pdf-export/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
<template>
<el-dialog
v-model="dialogVisible"
:title="$t('chat.preview')"
style="overflow: auto"
width="80%"
:before-close="close"
destroy-on-close
>
<div
v-loading="loading"
style="height: calc(70vh - 150px); overflow-y: auto; display: flex; justify-content: center"
>
<div ref="svgContainerRef"></div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button :loading="loading" @click="exportPDF">{{ $t('chat.exportPDF') }}</el-button>
<el-button
:loading="loading"
type="primary"
@click="
() => {
loading = true
exportJepg()
}
"
>
{{ $t('chat.exportImg') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import * as htmlToImage from 'html-to-image'
import { ref, nextTick } from 'vue'
import html2Canvas from 'html2canvas'
import { jsPDF } from 'jspdf'
const loading = ref<boolean>(false)
const svgContainerRef = ref()
const dialogVisible = ref<boolean>(false)
const open = (element: HTMLElement | null) => {
dialogVisible.value = true
loading.value = true
if (!element) {
return
}
setTimeout(() => {
nextTick(() => {
htmlToImage
.toSvg(element, { pixelRatio: 1, quality: 1 })
.then((dataUrl) => {
return fetch(dataUrl)
.then((response) => {
return response.text()
})
.then((text) => {
const parser = new DOMParser()
const svgDoc = parser.parseFromString(text, 'image/svg+xml')
const svgElement = svgDoc.documentElement
svgContainerRef.value.appendChild(svgElement)
svgContainerRef.value.style.height = svgElement.scrollHeight + 'px'
})
})
.finally(() => {
loading.value = false
})
})
}, 1)
}
const exportPDF = () => {
loading.value = true
setTimeout(() => {
nextTick(() => {
html2Canvas(svgContainerRef.value, {
logging: false,
})
.then((canvas) => {
const doc = new jsPDF('p', 'mm', 'a4')
// 将canvas转换为图片
const imgData = canvas.toDataURL(`image/jpeg`, 1)
// 获取PDF页面尺寸
const pageWidth = doc.internal.pageSize.getWidth()
const pageHeight = doc.internal.pageSize.getHeight()
// 计算图像在PDF中的尺寸
const imgWidth = pageWidth
const imgHeight = (canvas.height * imgWidth) / canvas.width
// 添加图像到PDF
doc.addImage(imgData, 'jpeg', 0, 0, imgWidth, imgHeight)
// 如果内容超过一页,自动添加新页面
let heightLeft = imgHeight
let position = 0
// 第一页已经添加
heightLeft -= pageHeight
// 当内容超过一页时
while (heightLeft >= 0) {
position = heightLeft - imgHeight
doc.addPage()
doc.addImage(imgData, 'jpeg', 0, position, imgWidth, imgHeight)
heightLeft -= pageHeight
}
// 保存PDF
doc.save('导出文档.pdf')
return 'ok'
})
.finally(() => {
loading.value = false
})
})
})
}
const exportJepg = () => {
loading.value = true
setTimeout(() => {
nextTick(() => {
html2Canvas(svgContainerRef.value, {
logging: false,
})
.then((canvas) => {
// 将canvas转换为图片
const imgData = canvas.toDataURL(`image/jpeg`, 1)
const link = document.createElement('a')
link.download = `webpage-screenshot.jpeg`
link.href = imgData
document.body.appendChild(link)
link.click()
return 'ok'
})
.finally(() => {
loading.value = false
})
})
}, 1)
}
const close = () => {
dialogVisible.value = false
}
defineExpose({ open, close })
</script>
<style lang="scss" scoped></style>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The provided Vue component appears to be designed to create a dialog with SVG content that allows users to export it as PDF or image. Here is a few points of consideration for optimization:

  1. SVG Loading: The current approach involves converting an SVG string into an HTML element, which can lead to some overhead due to parsing and appending. Consider using a more direct method to handle inline SVG files directly.

  2. Error Handling: Implement robust error handling in the fetch operations and other asynchronous calls to improve user experience. This includes providing meaningful messages when exports fail.

  3. Memory Management: Ensure that SVG elements are removed from memory after they are no longer needed to prevent excessive memory usage.

  4. PDF Export Optimization:

    • Instead of manually calculating page sizes, consider using pagination libraries like jspdf-autotable to manage text wrapping and formatting automatically.
    • Optimize SVG images before sending them to the server by reducing their size and resolution where appropriate.
  5. Code Readability: Add comments and structure the code better to make it easier to understand at a glance.

Here's a revised version with these considerations:

<template>
  <el-dialog
    v-model="dialogVisible"
    :title="$t('chat.preview')"
    style="overflow: auto"
    width="80%"
    :before-close="close"
    destroy-on-close
  >
    <div ref="content" v-loading="loading">Content will load here...</div>
    <template #footer>
      <span class="dialog-footer">
        <el-button :loading="loading" @click="exportPDF">{{ $t('chat.exportPDF') }}</el-button>
        <el-button
          :loading="loading"
          type="primary"
          @click="
            () => {
              loading = true
              exportJpg()
            }
          "
        >
          {{ $t('chat.exportImg') }}
        </el-button>
      </span>
    </template>
  </el-dialog>
</template>

<script setup lang="ts">
import { onMounted, ref, nextTick } from 'vue'

const content = ref('')
const loading = ref(false)

// Function to open the dialog and populate the SVG container
const open = async (svgBlob: Blob) => {
  try {
    dialogVisible.value = true
    loading.value = true

    // Convert blob to data URL
    const dataURL = await convertBlobToDataUrl(svgBlob)

    // Set contents of div to the data URL
    content.value.innerHTML = `<img src="${dataURL}" alt="Preview">`

    nextTick(() => {
      loading.value = false
    })
  } catch (error) {
    console.error("Failed to load SVG:", error)
    alert("Could not load the preview.")
  }
}

// Helper function to convert blob to data URL
const convertBlobToDataUrl = async (blob: Blob): Promise<string> => {
  return await new Promise((resolve) => {
    const reader = new FileReader();
    reader.onloadend = (_) => resolve(reader.result as string);
    reader.readAsDataURL(blob);
  });
};

// Handle closing the dialog
const close = () => {
  dialogVisible.value = false;
}

// Expose functions
defineExpose({ open, close });
</script>

<style lang="scss" scoped>
.dialog-container {
  /* Styles */
}
</style>

Key Changes:

  • The SVG content is loaded into an <img> tag within a hidden div (ref="content"), which simplifies rendering.
  • Error handling is added to capture and display errors during SVG conversion.
  • A helper function convertBlobToDataUrl is created to asynchronously read the blob content as a data URL.
  • Memory management is improved by cleaning up resources appropriately.

3 changes: 3 additions & 0 deletions ui/src/locales/lang/en-US/ai-chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ export default {
only20history: 'Showing only the last 20 chats',
question_count: 'Questions',
exportRecords: 'Export Chat History',
exportPDF: 'Export PDF',
exportImg: 'Exporting images',
preview: 'Preview',
chatId: 'Chat ID',
userInput: 'User Input',
quote: 'Quote',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The provided code snippet appears to be part of an object exported as default from a JavaScript module, likely for use in internationalization (i18n) purposes. Here's some general feedback:

Potential Issues:

  1. Missing Language Tags: The code doesn't specify the language tags (e.g., 'en', 'fr') associated with these strings.

Optimization Suggestions:

  1. Consistent Naming Conventions:
    • Ensure all keys and values are consistent. In this case, they look well-named but might need minor adjustments if different languages require specific names or formats.
  2. String Translation Considerations:
    • For localized content, ensure that each string is translated appropriately into multiple languages when deploying the application.
  3. Internationalization Libraries:
    • If using internationalization libraries like react-intl or lingui, consider integrating them effectively to handle translation without repetitive key usage.

Overall, the code looks clean and self-contained, which is good for its purpose in managing localization data. Make sure the translations are up-to-date for accuracy and completeness.

Expand Down
3 changes: 3 additions & 0 deletions ui/src/locales/lang/zh-CN/ai-chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ export default {
only20history: '仅显示最近 20 条对话',
question_count: '条提问',
exportRecords: '导出聊天记录',
exportPDF: '导出PDF',
exportImg: '导出图片',
preview: '预览',
chatId: '对话 ID',
chatUserId: '对话用户 ID',
chatUserType: '对话用户类型',
Expand Down
3 changes: 3 additions & 0 deletions ui/src/locales/lang/zh-Hant/ai-chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ export default {
only20history: '僅顯示最近 20 條對話',
question_count: '條提問',
exportRecords: '導出聊天記錄',
exportPDF: '匯出PDF',
exportImg: '匯出圖片',
preview: '預覽',
chatId: '對話 ID',
userInput: '用戶輸入',
quote: '引用',
Expand Down
56 changes: 0 additions & 56 deletions ui/src/utils/htmlToPdf.ts

This file was deleted.

13 changes: 8 additions & 5 deletions ui/src/views/chat/pc/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@
<el-dropdown-item @click="exportHTML"
>{{ $t('common.export') }} HTML</el-dropdown-item
>
<el-dropdown-item @click="exportToPDF('chatListId', currentChatName + '.pdf')"
<el-dropdown-item @click="openPDFExport"
>{{ $t('common.export') }} PDF</el-dropdown-item
>
</el-dropdown-menu>
Expand Down Expand Up @@ -169,7 +169,7 @@
<div class="execution-detail-panel" :resizable="false" collapsible>
<div class="p-16 flex-between border-b">
<h4 class="medium ellipsis" :title="rightPanelTitle">{{ rightPanelTitle }}</h4>
 

<div class="flex align-center">
<span v-if="rightPanelType === 'paragraphDocument'" class="mr-4">
<a
Expand Down Expand Up @@ -217,6 +217,7 @@
emitConfirm
@confirm="handleResetPassword"
></ResetPassword>
<PdfExport ref="pdfExportRef"></PdfExport>
</div>
</template>

Expand All @@ -238,12 +239,14 @@ import ParagraphDocumentContent from '@/components/ai-chat/component/knowledge-s
import HistoryPanel from '@/views/chat/component/HistoryPanel.vue'
import { cloneDeep } from 'lodash'
import { getFileUrl } from '@/utils/common'
import { exportToPDF } from '@/utils/htmlToPdf'
import PdfExport from '@/components/pdf-export/index.vue'
useResize()

const pdfExportRef = ref<InstanceType<typeof PdfExport>>()
const { common, chatUser } = useStore()
const router = useRouter()

const openPDFExport = () => {
pdfExportRef.value?.open(document.getElementById('chatListId'))
}
const isCollapse = ref(false)
const isPcCollapse = ref(false)
watch(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. There appears to be an extra line before </div> in the <div class="p-16 flex-between border-b"> section.

  2. In the <template> portion of your code, you have included an instance of the ResetPassword component with props emitConfirm and a handler function called handleResetPassword. Also note that there is a commented-out element ( ) which may not serve any purpose unless it's intentional.

  3. The template contains two references: one named pdfExportRef, though it doesn't seem to be used anywhere within this template, which could lead to potential memory leaks if not properly managed.

  4. You've defined a watcher on the isPcCollapse variable without providing a callback function. This watcher seems unnecessary and might cause side effects if no appropriate logic is implemented.

  5. If you're using Vue 3 Composition API and TypeScript, make sure all components imported are correctly typed (using <type> ImportComponentName). For example:

import type { Ref } from 'vue';
import { ref } from 'vue';

const pdfExportRef: Ref<InstanceType<typeof PdfExport>> | null = ref(null);

The rest of your code looks well-written except for those issues mentioned above.

Expand Down
Loading