Pārlūkot izejas kodu

feat: 优化下载链接的展示方式

zhupei@smartai.com 1 gadu atpakaļ
vecāks
revīzija
4e52afadaf

+ 37 - 0
src/modules/answer-refference/DownloadFileItem.vue

@@ -0,0 +1,37 @@
+<template>
+  <FileItemDocDownload :file="file" />
+</template>
+<script setup>
+import { defineComponent, inject, ref, useTemplateRef, onMounted, computed } from 'vue';
+
+import FileItemDocDownload from '../file-preview/FileItemDocDownload.vue';
+
+const props = defineProps({
+  file: Object
+});
+
+onMounted(() => {});
+</script>
+
+<style scoped>
+.refference-content {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+}
+.refference-description {
+  flex-grow: 1;
+}
+
+.ref-text-content {
+  max-width: 460px;
+  max-height: 340px;
+  overflow-y: auto;
+}
+</style>
+<style>
+.arco-drawer-container[data-flag='doc-ref-effect-contaienr'] .arco-drawer-body {
+  overflow: hidden;
+  padding: 0;
+}
+</style>

+ 37 - 0
src/modules/answer-refference/DownloadFileList.vue

@@ -0,0 +1,37 @@
+<template>
+  <DownloadFileItem v-for="(item, index) of files" :key="index" :file="item" />
+</template>
+<script setup>
+import { defineComponent, inject, ref, useTemplateRef, onMounted, computed } from 'vue';
+
+import DownloadFileItem from './DownloadFileItem.vue';
+
+const props = defineProps({
+  files: Array
+});
+
+onMounted(() => {});
+</script>
+
+<style scoped>
+.refference-content {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+}
+.refference-description {
+  flex-grow: 1;
+}
+
+.ref-text-content {
+  max-width: 460px;
+  max-height: 340px;
+  overflow-y: auto;
+}
+</style>
+<style>
+.arco-drawer-container[data-flag='doc-ref-effect-contaienr'] .arco-drawer-body {
+  overflow: hidden;
+  padding: 0;
+}
+</style>

+ 38 - 0
src/modules/answer-refference/apply-refference-popover-4-download-file.js

@@ -0,0 +1,38 @@
+import dynamicEffectDownloadLinks from './effect-download-links';
+
+/**
+ * 文件下载
+ * @param {string} container_klass
+ * @param {object[]} files - 文件数据
+ * @param {object} files.file_name - 文件名
+ * @param {object} files.file_url - 下载链接
+ * @returns
+ */
+export default function applyRefferencePopover4DownloadFile(container_klass, files) {
+  let unmount = () => {};
+
+  // 处理引用数据
+  setTimeout(() => {
+    const selector = `.${container_klass} .agent-answer-download-container`;
+
+    // 无序号,渲染所有的内容
+    const ele = document.querySelectorAll(selector);
+
+    renderFlag(ele);
+  }, 0);
+
+  function renderFlag(ele) {
+    // 遍历找到的所有标记元素
+    ele.forEach((item, idx) => {
+      if (files && Array.isArray(files) && files.length !== 0) {
+        unmount = dynamicEffectDownloadLinks(item, files);
+      }
+    });
+  }
+
+  return () => {
+    if (typeof unmount === 'function') {
+      unmount();
+    }
+  };
+}

+ 33 - 0
src/modules/answer-refference/effect-download-links.js

@@ -0,0 +1,33 @@
+import { createApp } from 'vue';
+
+import ArcoVue from '@arco-design/web-vue';
+import ArcoVueIcon from '@arco-design/web-vue/es/icon';
+
+import DownloadFileList from './DownloadFileList.vue';
+
+/**
+ * 创建一个vue APP,并将此APP挂载到对应的DOM节点中
+ * @param {Element} mount_element - 挂载点元素
+ * @param {Object[]} files - props值
+ * @param {object} files.file_name - 文件名
+ * @param {object} files.file_url - 下载链接
+ */
+export default function dynamicEffectDownloadLinks(mount_element, files = []) {
+  /**
+   * @see https://cn.vuejs.org/api/application.html#createapp
+   */
+  const app = createApp(DownloadFileList, {
+    files: files
+  });
+
+  // 引用字节跳动相关的组件
+  app.use(ArcoVueIcon);
+  app.use(ArcoVue);
+
+  app.mount(mount_element);
+
+  // 返回一个卸载APP的方法
+  return () => {
+    app.unmount();
+  };
+}

+ 2 - 1
src/modules/conversation-chat/DialogItem.vue

@@ -85,7 +85,8 @@ const answerInfo = computed(() => {
   // obj.client_custom_linked_code = chat_info.client_custom_linked_code || '';
   // obj.client_custom_linked_refference_full_content = chat_info.client_custom_linked_refference_full_content || '';
   // obj.client_custom_linked_image = chat_info.client_custom_linked_image || '';
-  // obj.client_custom_linked_download_files = chat_info.client_custom_linked_download_files || '';
+
+  obj.client_custom_linked_download_files = chat_info.client_custom_linked_download_files || '';
   obj.client_custom_linked_refference_full_content = chat_info.client_custom_linked_refference_full_content; // 完整引用地址
 
   obj.client_custom_attached_html_before_render = chat_info.client_custom_attached_html_before_render;

+ 56 - 0
src/modules/conversation-dialog/format-dialog-answer-attached-html-before-render.js

@@ -0,0 +1,56 @@
+import {
+  formatRefferenceString,
+  formatRefferenceFlag,
+  formatFileDownloadStr
+} from './format-dialog-answer-custom-markdown-str';
+
+/**
+ * 拼接到智能体回复后面的html字符串
+ * 智能体的回答,最终呈现到界面上,共有如下几个步骤:
+ * 1. 智能体回复的markdown
+ * 2. 将markdown解析为html文件
+ * 3. 在解析后的html字符串后,拼接一些特殊情况下的其他html字符串
+ *
+ * 此方法所做的事情就是生成上述步骤3所需要的 特殊情况下的其他html字符串
+ * 最终将该html字符串,赋给 client_custom_attached_html_before_render 属性
+ * 在 ../conversation-item/DialogAnswer.vue 组件中,会进行上述步骤2和步骤3的处理
+ * @param {object} currentMsg - 前一次的消息,如果是流式消息,则需要上一次的Content进行拼接,从而形成新的消息
+ * @returns
+ */
+export default function formatDialogAnswerAttachedHtmlBeforeRender(currentMsg) {
+  /**
+   * 在answer字符串渲染到界面之前,在answer后拼接的html字符串
+   */
+  let temp_html_str = '';
+
+  // 处理python代码
+  if (currentMsg.client_custom_linked_code) {
+    temp_html_str = temp_html_str + currentMsg.client_custom_linked_code;
+  }
+
+  // 由于SQL和代码不能通过markdown解析,所以此处进行字符串的拼接
+  if (currentMsg.client_custom_linked_sql) {
+    temp_html_str = temp_html_str + currentMsg.client_custom_linked_sql;
+  }
+
+  // 处理图片展示
+  if (currentMsg.client_custom_linked_image) {
+    temp_html_str = temp_html_str + `<img src="${currentMsg.client_custom_linked_image}" />`;
+  }
+  // 拼接文件的下载链接
+  if (currentMsg.client_custom_linked_download_files) {
+    temp_html_str = temp_html_str + `<div class="agent-answer-download-container"></div>`;
+
+    // const file_name = currentMsg.client_custom_linked_download_files.file_name;
+    // const file_url = currentMsg.client_custom_linked_download_files.file_url;
+    // const download_file_str = formatFileDownloadStr(file_name, file_url);
+    // temp_html_str = temp_html_str + download_file_str;
+  }
+
+  /**
+   * 在answer字符串渲染到界面之前,在answer后拼接的html字符串
+   */
+  currentMsg.client_custom_attached_html_before_render = temp_html_str;
+
+  return currentMsg;
+}

+ 61 - 5
src/modules/conversation-dialog/format-dialog-answer-from-history.js

@@ -1,11 +1,11 @@
 import generateClientUniqueId from '../../utils/generate-client-unique-id';
-import { jsonParse, jsonStringify } from '../../utils/utils';
+// import { jsonParse, jsonStringify } from '../../utils/utils';
 
 import { formatCodePython, formatCodeSql } from './format-code-string';
 import { getIcon4Answer } from './get-icon-4-dialog';
 
-import formatAnwserString from './format-anwser-string';
-import { formatStaticAssetsUrl } from '../../utils/utils';
+// import formatAnwserString from './format-anwser-string';
+// import { formatStaticAssetsUrl } from '../../utils/utils';
 
 import {
   formatRefferenceString,
@@ -13,8 +13,13 @@ import {
   formatFileDownloadStr
 } from './format-dialog-answer-custom-markdown-str';
 
+import formatDialogAnswerAttachedHtmlBeforeRender from './format-dialog-answer-attached-html-before-render';
+
 /**
  * 格式化用户的对话记录的回答数据
+ * 请注意,通常情况下,此文件改动,意味着 ./format-dialog-answer-from-websocket_receiver-message.js 也需要改动
+ * 此文件是针对历史会话中的答案的字符串进行处理
+ * ./format-dialog-answer-from-websocket_receiver-message.js 文件是针对实时会话回答的字符串进行处理
  */
 export default function formatDialogAnswerFromHistory(item, agentInfo) {
   const icon_src = getIcon4Answer(agentInfo);
@@ -122,10 +127,27 @@ export default function formatDialogAnswerFromHistory(item, agentInfo) {
   }
 
   if (resFiles) {
+    // 历史会话也有此同样处理
     if (resFiles[0]) {
+      const fileInfo = resFiles[0];
+      const f_url = fileInfo.file_url;
+      const mock_url = `http://127.0.0.1${f_url}`;
+      const urlObj = new URL(mock_url);
+      const file_type = urlObj.searchParams.get('file_type');
+
+      let f_name = fileInfo.file_name;
+      if (file_type === 'word') {
+        f_name = f_name + '.docx';
+      } else {
+        // 对于非 word类型,未知文件名
+      }
+
+      /**
+       * 2024/12/27 17:09 后端说:目前仅有docx,文件后缀需要解析,让前端自己根据file_url中的file_type进行解析
+       */
       history_item.client_custom_linked_download_files = {
-        file_name: resFiles[0].file_name,
-        file_url: resFiles[0].file_url
+        file_name: f_name,
+        file_url: fileInfo.file_url
       };
     }
   }
@@ -157,5 +179,39 @@ export default function formatDialogAnswerFromHistory(item, agentInfo) {
     history_item.client_custom_linked_refference_full_content = resRefference;
   }
 
+  // /**
+  //  * 在answer字符串渲染到界面之前,在answer后拼接的html字符串
+  //  */
+  // let temp_html_str = '';
+
+  // // 处理python代码
+  // if (history_item.client_custom_linked_code) {
+  //   temp_html_str = temp_html_str + history_item.client_custom_linked_code;
+  // }
+
+  // // 由于SQL和代码不能通过markdown解析,所以此处进行字符串的拼接
+  // if (history_item.client_custom_linked_sql) {
+  //   temp_html_str = temp_html_str + history_item.client_custom_linked_sql;
+  // }
+
+  // // 处理图片展示
+  // if (history_item.client_custom_linked_image) {
+  //   temp_html_str = temp_html_str + `<img src="${history_item.client_custom_linked_image}" />`;
+  // }
+  // // 拼接文件的下载链接
+  // if (history_item.client_custom_linked_download_files) {
+  //   const file_name = history_item.client_custom_linked_download_files.file_name;
+  //   const file_url = history_item.client_custom_linked_download_files.file_url;
+  //   const download_file_str = formatFileDownloadStr(file_name, file_url);
+  //   temp_html_str = temp_html_str + download_file_str;
+  // }
+
+  // /**
+  //  * 在answer字符串渲染到界面之前,在answer后拼接的html字符串
+  //  */
+  // history_item.client_custom_attached_html_before_render = temp_html_str;
+
+  formatDialogAnswerAttachedHtmlBeforeRender(history_item);
+
   return history_item;
 }

+ 57 - 35
src/modules/conversation-dialog/format-dialog-answer-from-websocket_receive_message.js

@@ -1,9 +1,9 @@
 import { jsonParse, jsonStringify } from '../../utils/utils';
-import { formatStaticAssetsUrl } from '../../utils/utils';
+// import { formatStaticAssetsUrl } from '../../utils/utils';
 import generateClientUniqueId from '../../utils/generate-client-unique-id';
 
 import { formatCodePython, formatCodeSql } from './format-code-string';
-import formatAnwserString, { formatMarkdownString } from './format-anwser-string';
+// import formatAnwserString, { formatMarkdownString } from './format-anwser-string';
 // import { formatFileDownloadStr } from './format-dialog-answer-custom-markdown-str';
 import { getIcon4Answer } from './get-icon-4-dialog';
 
@@ -13,9 +13,15 @@ import {
   formatFileDownloadStr
 } from './format-dialog-answer-custom-markdown-str';
 
+import formatDialogAnswerAttachedHtmlBeforeRender from './format-dialog-answer-attached-html-before-render';
+
 /**
  * 格式化websocket发送消息的参数
  * 此方法返回的数据项,为智能体返回的回答的数据
+ *
+ * 请注意,通常情况下,此文件改动,可能意味着 ./format-dialog-answer-from-history.js 文件也需要改动
+ * 此文件是针对 实时对话 进行的回答字符串的处理
+ * ./format-dialog-answer-from-history.js 文件是针对历史会话回答的字符串进行处理
  * @param {string} message_str
  * @param {object} preMsg - 前一次的消息,如果是流式消息,则需要上一次的Content进行拼接,从而形成新的消息
  * @returns
@@ -232,11 +238,25 @@ export default function formatDialogAnswerFromWebsocketReceiveMessage(message_st
     }
 
     if (resFiles) {
+      // 历史记录也有此同样处理
       // 文档合并有此数据
       if (resFiles[0]) {
+        const fileInfo = resFiles[0];
+        const f_url = fileInfo.file_url;
+        const mock_url = `http://127.0.0.1${f_url}`;
+        const urlObj = new URL(mock_url);
+        const file_type = urlObj.searchParams.get('file_type');
+
+        let f_name = fileInfo.file_name;
+        if (file_type === 'word') {
+          f_name = f_name + '.docx';
+        } else {
+          // 对于非 word类型,未知文件名
+        }
+
         currentMsg.client_custom_linked_download_files = {
-          file_name: resFiles[0].file_name,
-          file_url: resFiles[0].file_url
+          file_name: f_name,
+          file_url: fileInfo.file_url
         };
       }
     }
@@ -314,37 +334,39 @@ export default function formatDialogAnswerFromWebsocketReceiveMessage(message_st
   // ---------------------------------------------------------------
   // ---------------------------------------------------------------
 
-  /**
-   * 在answer字符串渲染到界面之前,在answer后拼接的html字符串
-   */
-  let temp_html_str = '';
-
-  // 处理python代码
-  if (currentMsg.client_custom_linked_code) {
-    temp_html_str = temp_html_str + currentMsg.client_custom_linked_code;
-  }
-
-  // 由于SQL和代码不能通过markdown解析,所以此处进行字符串的拼接
-  if (currentMsg.client_custom_linked_sql) {
-    temp_html_str = temp_html_str + currentMsg.client_custom_linked_sql;
-  }
-
-  // 处理图片展示
-  if (currentMsg.client_custom_linked_image) {
-    temp_html_str = temp_html_str + `<img src="${currentMsg.client_custom_linked_image}" />`;
-  }
-  // 拼接文件的下载链接
-  if (currentMsg.client_custom_linked_download_files) {
-    const file_name = currentMsg.client_custom_linked_download_files.file_name;
-    const file_url = currentMsg.client_custom_linked_download_files.file_url;
-    const download_file_str = formatFileDownloadStr(file_name, file_url);
-    temp_html_str = temp_html_str + download_file_str;
-  }
-
-  /**
-   * 在answer字符串渲染到界面之前,在answer后拼接的html字符串
-   */
-  currentMsg.client_custom_attached_html_before_render = temp_html_str;
+  // /**
+  //  * 在answer字符串渲染到界面之前,在answer后拼接的html字符串
+  //  */
+  // let temp_html_str = '';
+
+  // // 处理python代码
+  // if (currentMsg.client_custom_linked_code) {
+  //   temp_html_str = temp_html_str + currentMsg.client_custom_linked_code;
+  // }
+
+  // // 由于SQL和代码不能通过markdown解析,所以此处进行字符串的拼接
+  // if (currentMsg.client_custom_linked_sql) {
+  //   temp_html_str = temp_html_str + currentMsg.client_custom_linked_sql;
+  // }
+
+  // // 处理图片展示
+  // if (currentMsg.client_custom_linked_image) {
+  //   temp_html_str = temp_html_str + `<img src="${currentMsg.client_custom_linked_image}" />`;
+  // }
+  // // 拼接文件的下载链接
+  // if (currentMsg.client_custom_linked_download_files) {
+  //   const file_name = currentMsg.client_custom_linked_download_files.file_name;
+  //   const file_url = currentMsg.client_custom_linked_download_files.file_url;
+  //   const download_file_str = formatFileDownloadStr(file_name, file_url);
+  //   temp_html_str = temp_html_str + download_file_str;
+  // }
+
+  // /**
+  //  * 在answer字符串渲染到界面之前,在answer后拼接的html字符串
+  //  */
+  // currentMsg.client_custom_attached_html_before_render = temp_html_str;
+
+  formatDialogAnswerAttachedHtmlBeforeRender(currentMsg);
 
   return {
     status: chat_status, // 本轮会话的状态:0 未开始 1 进行中 2 已结束 3 错误

+ 7 - 0
src/modules/conversation-item/DialogAnswer.vue

@@ -61,6 +61,7 @@ import { formatFileDownloadStr } from '../conversation-dialog/format-dialog-answ
 import { getIconIntelligent } from '../common/icon-map';
 import { formatStaticAssetsUrl } from '../../utils/utils';
 
+import applyRefferencePopover4DownloadFile from '../answer-refference/apply-refference-popover-4-download-file';
 import applyRefferencePopover4RefFlag from '../answer-refference/apply-refference-popover-4-ref-flag';
 import applyRefferencePopover4FileName from '../answer-refference/apply-refference-popover-4-filename';
 import { applyCodeHighLight } from '../conversation-dialog/format-code-string';
@@ -263,12 +264,18 @@ onMounted(() => {
   const answer = props.answer;
 
   const full_refference = answer.client_custom_linked_refference_full_content;
+  const download_files = answer.client_custom_linked_download_files;
 
   /**
    * 文档引用效果
    */
   applyRefferencePopover4RefFlag(container_klass, full_refference);
   // applyRefferencePopover4FileName(container_klass, full_refference);
+
+  if (download_files) {
+    applyRefferencePopover4DownloadFile(container_klass, [download_files]);
+  }
+
   /**
    * 处理代码样式: 美化代码的展示
    */

+ 138 - 0
src/modules/file-preview/FileItemDocDownload.vue

@@ -0,0 +1,138 @@
+<template>
+  <div class="file-item-doc-preview" @click="handleFileDownload">
+    <FileIcon :file_end_fix="file_end_fix" class="file-icon-box" />
+
+    <div class="file-item-info-container">
+      <a-tooltip content="点击下载文件" position="top">
+        <div class="file-item-doc-title">{{ file.file_name }}</div>
+      </a-tooltip>
+
+      <!-- <FileSize :file_size="file.size" /> -->
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, onMounted } from 'vue';
+import FileIcon from '../file-icon/FileIcon.vue';
+// import FileSize from './FileSize.vue';
+// import FileItemRemoveBtn from './FileItemRemoveBtn.vue';
+
+// import ViewerPDFFromFile from '../viewer-pdf/ViewerPDFFromFile.vue';
+// import ViewerDocxFromFile from '../viewer-docx/ViewerDocxFromFile.vue';
+
+import { getFileSuffix } from '../../utils/file-utils';
+
+const props = defineProps({
+  file: Object
+  // enable_remove: Boolean
+});
+const emit = defineEmits(['delete']);
+
+const DOC_WIDTH = window.innerWidth / 2;
+
+/**
+ * 文档类别:doc/pdf
+ */
+const doc_type = computed(() => {
+  const doc_name = props.file.file_name;
+
+  if (!doc_name) {
+    return '';
+  }
+
+  // const arr = doc_name.split('.');
+  // const len = arr.length;
+  // const end_fix = arr[len - 1];
+
+  const end_fix = getFileSuffix(doc_name);
+
+  if (end_fix === 'pdf') {
+    return 'pdf';
+  }
+
+  if (end_fix === 'docx' || end_fix === 'doc') {
+    return 'doc';
+  }
+
+  return '';
+});
+
+const visible = ref(false);
+
+/**
+ * 文件后缀名
+ */
+const file_end_fix = computed(() => {
+  const file = props.file;
+
+  if (!file) {
+    return '';
+  }
+
+  const file_name = file.file_name || '';
+
+  const end_fix = getFileSuffix(file_name);
+
+  return end_fix;
+});
+
+/**
+ * 文件下载
+ */
+const handleFileDownload = () => {
+  if (props.file.file_url) {
+    window.open(props.file.file_url, '_blank');
+  }
+};
+
+onMounted(() => {});
+</script>
+
+<style lang="css" scoped>
+.file-item-doc-preview {
+  /* background-color: #fff; */
+  /* padding: 5px; */
+
+  position: relative;
+
+  display: flex;
+
+  align-items: center;
+
+  /* padding: 8px; */
+  /* padding-right: 20px; */
+
+  background-color: var(--color-bg-1);
+  color: var(--color-text-2);
+
+  border-radius: 8px;
+
+  max-width: 230px;
+}
+
+.file-item-remove-button {
+  top: 3px;
+  right: 3px;
+}
+
+.file-item-info-container {
+  margin-left: 5px;
+}
+
+.file-item-doc-title {
+  margin-bottom: 5px;
+
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 1; /* 这里设置显示的行数 */
+  line-clamp: 1;
+  overflow: hidden;
+
+  cursor: pointer;
+}
+
+.file-icon-box {
+  flex-shrink: 0;
+}
+</style>