浏览代码

perf: 优化对话中答案的展示

1. 调整文档引用效果和图片展示效果的实现方式
2. 拆离 Anwser.vue 组件,将答案数据单独处理
zhupei 1 年之前
父节点
当前提交
49e4d340b7

+ 54 - 14
src/components/DialogWrap.vue

@@ -22,13 +22,18 @@
       <Suggestion :suggestions="suggestions" :readonly="readonly"/>
 
       <div class="dialog-list" v-if="dialogList.length > 0" :key="randomKey">
-        <div :class="['dialog-item', isSetting ? '' : 'dialog-absolute', 'dialog-item-' + key]" v-for="(item, key) in dialogList"
-          :key="item.key" :data-item-index="key">
+        <div 
+          :class="['dialog-item', isSetting ? '' : 'dialog-absolute', 'dialog-item-' + key]" 
+          v-for="(item, key) in dialogList"
+          :key="item.key" 
+          :data-item-index="key"
+        >
           <div class="dialog-header" v-if="item?.question">
             <div class="user-name" style="background-color: rgb(255, 110, 110)">
               {{ userName.slice(0, 1) }}
             </div>
           </div>
+
           <div class="dialog-footer" v-if="item?.question">
             <p style="margin-top: 0">{{ item.question }}</p>
           </div>
@@ -49,6 +54,7 @@
               <span v-html="item.sql"></span>
             </div>
           </div>
+
           <div class="dialog-footer" v-else-if="item?.capacity"
             :style="{ display: dialogConfig.type === 'paper' ? 'inline-block' : 'block' }">
             <div v-if="type === 'knowledge'" @click="openCapacity(key)"
@@ -65,6 +71,7 @@
               <a-spin :size="18" />
               <span>{{ type === 'excel' ? '正在为您合并...' : '正在为您搜索答案...' }}</span>
             </div>
+
             <section v-if="type === 'knowledge' && item.capacity.status === 'loaded'" v-show="menuList[key].open">
               <div v-if="item.capacity.data.list">
                 <p v-for="(item, key) in item.capacity.data.list" :key="key">
@@ -73,6 +80,7 @@
                   </a-typography-paragraph>
                 </p>
               </div>
+
               <div v-if="item.capacity.data.documents">
                 <div v-for="(document, documentKey) in item.capacity.data.documents" :key="documentKey">
                   <div @click="openDocument(key, documentKey)" :class="[readonly ? 'disabled' : '']">
@@ -114,12 +122,30 @@
                   <em>DOCX,13.19KB</em>
                 </span>
               </span>
-              <span v-else v-html="getHTML(item.answer)"></span>
+              <!-- <span v-else v-html="getHTML(item.answer)"></span> -->
+
+              <Answer v-else 
+                :answer="item.answer" 
+                :index="key" 
+                :chat_id="dialogId" 
+                :chat_type="type" 
+                :agentId="agentId"
+                :special_refference="item.fullRefference"
+              />
             </p>
           </div>
 
           <div class="dialog-footer" v-else>
-            <span v-html="getHTML(item.answer)"></span>
+            <!-- <span v-html="getHTML(item.answer)"></span> -->
+
+            <Answer 
+              :answer="item.answer" 
+              :index="key" 
+              :chat_id="dialogId" 
+              :chat_type="type" 
+              :agentId="agentId"
+              :special_refference="item.fullRefference"
+            />
           </div>
 
           <!-- 处理来源 -->
@@ -230,19 +256,23 @@ import DialogFeedback from "./DialogFeedback.vue";
 import { defineComponent, ref, onBeforeUnmount, onMounted, reactive, nextTick, inject } from "vue";
 
 import formatAnwserString from './refference-effect/format-anwser-string';
-import applyRefferencePopover from './refference-effect/apply-refference-popover';
-import applyImageDisplay from './refference-effect/apply-image-display';
+// import applyRefferencePopover from './refference-effect/apply-refference-popover';
+// import applyImageDisplay from './refference-effect/apply-image-display';
 import initTempHistory, {getChatIdFromUrl} from './refference-effect/temp-history';
 
 import { getDocumentUrl } from '../ajax/document';
 
 import Suggestion from './dialog-wrap/Suggestion.vue';
 
+import Answer from './dialog-wrap/Answer.vue';
+
 export default defineComponent({
   components: {
     DialogFeedback,
 
-    Suggestion
+    Suggestion,
+
+    Answer
   },
   props: [
     "open", 
@@ -292,6 +322,10 @@ export default defineComponent({
             content_ltks: [item.reference.chunks.find(chunk => chunk.doc_id == reference.doc_id).content_ltks]
           }
         }) : [],
+
+        // 完整的引用数据: pdf文档引用效果需要使用此数据
+        fullRefference: item.reference,
+
         // 如果有文件处理完拿第一个,后续有变动再改
         file: (item.files && item.files.length > 0) ? item.files.map(item => {
           const port = window.location.port ? `:${window.location.port}` : '';
@@ -424,6 +458,9 @@ export default defineComponent({
     const isLoaded = ref(false);
     const answerData = ref("");
     const answerReference = ref([]);
+    const fullRefference = reactive({
+      full_ref: {}
+    });
     const imageUrl = ref(undefined);
     const answerFile = ref(undefined);
     const answerCode = ref(undefined);
@@ -436,6 +473,7 @@ export default defineComponent({
     const addAnswer = (data) => {
       data.answer = answerData.value;
       data.reference = answerReference.value;
+      data.fullRefference = fullRefference.full_ref; // 完整的引用数据
       data.image_url = imageUrl.value;
       data.code = answerCode.value;
       data.file = answerFile.value;
@@ -464,8 +502,8 @@ export default defineComponent({
        * 应用引用效果
        * 答案中 引用图标(感叹号)点击时,浮层显示引用内容
        */
-      applyRefferencePopover(props.list);
-      applyImageDisplay(props.list);
+      // applyRefferencePopover(props.list);
+      // applyImageDisplay(props.list);
 
       /**
        * TODO: 只允许保存一条消息,避免出现多条消息同时发送的情况
@@ -578,6 +616,8 @@ export default defineComponent({
           message = replaceCode(message, cur_index);
           answerData.value = getHTML(message);
           answerReference.value = [];
+          
+          fullRefference.full_ref = reference; // 完整引用数据
 
           // 如有文档引用处理接收到的新消息和引用
           if (reference && reference.doc_aggs && reference.doc_aggs.length > 0) {
@@ -650,11 +690,11 @@ export default defineComponent({
           isLoaded.value = false;
         }
 
-        // 获取所有的临时历史记录
-        const full_history = tempHistory.getHistory();
-        applyRefferencePopover(full_history);
-        const current_index = tempHistory.getChatIndex(cur_chat_id);
-        applyImageDisplay(full_history, current_index);
+        // // 获取所有的临时历史记录
+        // const full_history = tempHistory.getHistory();
+        // applyRefferencePopover(full_history);
+        // const current_index = tempHistory.getChatIndex(cur_chat_id);
+        // applyImageDisplay(full_history, current_index);
       }
 
       helper.webSocket.onmessage = (event) => {

+ 73 - 0
src/components/dialog-wrap/Answer.vue

@@ -0,0 +1,73 @@
+<template>
+  <span v-html="display_answer" :class="anwser_klass"></span>
+</template>
+
+<script setup>
+import { ref, onBeforeUnmount, onMounted, reactive, nextTick, inject, computed } from "vue";
+
+import formatAnwserString from '../refference-effect/format-anwser-string';
+
+import applyRefferencePopover from '../refference-effect/apply-refference-popover';
+import applyImageDisplay from '../refference-effect/apply-image-display';
+
+const props = defineProps({
+    /**
+     * 回答的文本数据
+     */
+    answer: String,
+    /**
+     * 界面中对话的序号
+     */
+    index: Number,
+    /**
+     * 当前对话的ID值
+     */
+    chat_id: String,
+    /**
+     * 当前对话的类型
+     */
+    chat_type: String,
+    /**
+     * 当前对话使用的agent ID
+     */
+    agentId: String,
+
+    /**
+     * 特殊数据:专为文档引用效果提供的数据
+     * 文档引用功能需要使用此数据实现
+     */
+    special_refference: Object
+});
+
+const getHTML = (string) => {
+    return formatAnwserString(string);
+};
+
+const display_answer = computed(() => {
+    return getHTML(props.answer)
+});
+
+const anwser_klass = computed(() => {
+    return `${props.chat_type}-${props.agentId}-answer-content-${props.index}`;
+});
+
+onMounted(() => {
+    const container_klass = anwser_klass.value;
+
+    /**
+     * 小数绘图的文生图 图片效果
+     */
+    applyImageDisplay(container_klass);
+
+    /**
+     * 文档引用效果
+     */
+    applyRefferencePopover(container_klass, props.special_refference);
+});
+</script>
+<style>
+
+</style>
+<style scoped>
+
+</style>

+ 6 - 4
src/components/image-editor/TuiImageEditorVue3.vue

@@ -438,10 +438,12 @@ onMounted(() => {
     }
 
     editorInstance = new ImageEditor(editor.value, opts);
-    editorInstance.addImageObject(props.image_url).then(objectProps => {
-      // https://nhn.github.io/tui.image-editor/latest/ImageEditor#addImageObject
-      console.log(ojectProps.id, 'console.log(ojectProps.id);');
-    });
+    // editorInstance.addImageObject(props.image_url).then(objectProps => {
+    //   // https://nhn.github.io/tui.image-editor/latest/ImageEditor#addImageObject
+    //   console.log(ojectProps.id, 'console.log(ojectProps.id);');
+    // }).catch(err => {
+    //   console.log(err, 'error-出错了');
+    // });
     addEventListener();
 });
 

+ 8 - 38
src/components/refference-effect/apply-image-display.js

@@ -1,49 +1,19 @@
 import dynamicImageDisplay from './effect-image-display';
 
-export default function applyImageDisplay(dialogList, index) {
+export default function applyImageDisplay(container_klass) {
     let unmount = () => { };
 
-    /**
-     * 合法数据示例
-     * 
-    [
-        {
-            anwser: '',
-            question: ''
-        }
-    ]
-    */
-
     // 处理引用数据
     setTimeout(() => {
-        if (index !== undefined && typeof index === 'number' && index / 1 >= 0) {
-            // 有序号
-            // 查询会话数量
-            const ele_item = document.querySelectorAll('.dialog-item.dialog-item-' + index + ' .xiaoshuhuitu-image-container');
-
-            const len = ele_item.length;
-
-            for (let i = 0; i < len; i++) {
-
-                const ele = ele_item[i]
+        // 无序号,渲染所有的内容
+        const ele = document.querySelectorAll(`.${container_klass} .xiaoshuhuitu-image-container`);
 
-                if (ele.length === 0) {
-                    return;
-                }
-
-                renderFlag([ele]);
-            }
-        } else {
-            // 无序号,渲染所有的内容
-            const ele = document.querySelectorAll('.xiaoshuhuitu-image-container');
-
-            if (ele.length === 0) {
-                return;
-            }
-
-            renderFlag(ele);
+        if (ele.length === 0) {
+            return;
         }
-    }, 100);
+
+        renderFlag(ele);
+    }, 0);
 
     function renderFlag(ele) {
         // 遍历找到的所有标记元素

+ 7 - 62
src/components/refference-effect/apply-refference-popover.js

@@ -2,68 +2,20 @@ import dynamicRegRefFlag from './effect-refference-flag'
 
 /**
  * 应用PDF文档引用
- * @param {object[]} dialogList 
- * @param {number} index - 可选参数,当前渲染哪个会话 
+ * @param {string} container_klass 
+ * @param {object} refference - 引用数据
  * @returns 
  */
-export default function applyRefferencePopover(dialogList, index) {
+export default function applyRefferencePopover(container_klass, refference) {
     let unmount = () => { };
 
-    /**
-     * 合法数据示例
-     * 
-    [
-        {
-            reference: {
-                chunks: [
-                    {
-                        chunk_id:"9eb244e6943d486c573b35fc1e54aeec",
-                        content_ltks:"进国网项目“基于电磁时间反演的宽频电压感知关键技术研究及应用”试点应用9月13日— 14日,设备中心就国网科技项目“基于电磁时间反演的宽频电压感知关键技术研究及应用”在上海及海盐的试点应用开展了现场勘察,并根据现场实际情况制定了详细的应用实施方案。本项目由浙江公司牵头,设备中心负责课题1及课题3并深度参与课题4。为更好开展后续的宽频扰动及暂态电压分析实验室测试和现场验证工作,设备中心与市区公司合作,挑选35 kv提兰站的 10 kv线路作为应用对象。项目组分别对上海提兰站及海盐的试点应用工程进行了现场勘察,重点对后期现场应用时的具体方案进行了制定,明确了下一步的装置研发路线及注意事项。接下来,项目组将继续开展装置的理论研究及实物研制,并在实验室环境对所研制装置进行功能验证及安全性考核,完善现场应用实施方案。 (文:王劭菁) ",
-                        content_with_weight:"进国网项目“基于电磁时间反演的宽频电压感知关键技术研究及应用”试点应用9月13日—14日,设备中心就国网科技项目“基于电磁时间反演的宽频电压感知关键技术研究及应用”在上海及海盐的试点应用开展了现场勘察,并根据现场实际情况制定了详细的应用实施方案。本项目由浙江公司牵头,设备中心负责课题1及课题3并深度参与课题4。为更好开展后续的宽频扰动及暂态电压分析实验室测试和现场验证工作,设备中心与市区公司合作,挑选35kV提兰站的10kV线路作为应用对象。项目组分别对上海提兰站及海盐的试点应用工程进行了现场勘察,重点对后期现场应用时的具体方案进行了制定,明确了下一步的装置研发路线及注意事项。接下来,项目组将继续开展装置的理论研究及实物研制,并在实验室环境对所研制装置进行功能验证及安全性考核,完善现场应用实施方案。(文:王劭菁)",
-                        doc_id:"d91aef1e9c0211efae240242ac160006",
-                        docnm_kwd:"《设备技术中心工作动态》2023年第10期.pdf",
-                        img_id:"81b160609be811ef9da00242ac160006-9eb244e6943d486c573b35fc1e54aeec",
-                        "important_kwd": [],
-                        "kb_id": "81b160609be811ef9da00242ac160006",
-                        "positions": [
-                            [21.0, 75.0, 509.0, 458.0, 494.0],
-                            [21.0, 74.0, 508.0, 502.0, 665.0]
-                        ],
-                        "similarity": 0.8417970141691651,
-                        "term_similarity": 1.0,
-                        "vector_similarity": 0.47265671389721703
-                    }
-                ],
-            }
-        }
-    ]
-    */
-
     // 处理引用数据
     setTimeout(() => {
-        if (index !== undefined && typeof index === 'number' && index / 1 >= 0) {
-            // 有序号
-            // 查询会话数量
-            const ele_item = document.querySelectorAll('.dialog-item.dialog-absolute');
+        // 无序号,渲染所有的内容
+        const ele = document.querySelectorAll(`.${container_klass} .refflag-refflag-refflag`);
 
-            const len = ele_item.length;
-
-            for (let i = 0; i < len; i++) {
-                if (i === index) {
-                    // 开始渲染
-                    const ele = ele_item[i].querySelectorAll('.refflag-refflag-refflag');
-
-                    renderFlag(ele);
-                    break;
-                }
-            }
-        } else {
-            // 无序号,渲染所有的内容
-            const ele = document.querySelectorAll('.refflag-refflag-refflag');
-
-            renderFlag(ele);
-        }
-    }, 100);
+        renderFlag(ele);
+    }, 0);
 
     function renderFlag(ele) {
         // 遍历找到的所有标记元素
@@ -71,14 +23,7 @@ export default function applyRefferencePopover(dialogList, index) {
             const data_flag_index = item.getAttribute('data-flag-index');
             const data_list_index = item.getAttribute('data-list-index');
 
-            // 当前dialog对应的数据
-            const dialog_data = dialogList[data_list_index];
-
-            // 引用参考
-            const refference = dialog_data.reference;
-
             if (refference) {
-
                 const chunks = refference.chunks;
 
                 if (chunks) {

+ 9 - 3
src/components/refference-effect/format-anwser-string.js

@@ -28,15 +28,18 @@ export default function formatAnwserString(string) {
         // 已发现 ![] 字符串
     }
 
-
-
     const result = marked.parse(temp_str);
 
+    /**
+     * 将链接替换为新窗口打开
+     */
+    let parse_href = result.replace('<a href=', '<a target="_blank" href=');
+
     /**
      * 替换图片字符串
      * 将字符串中的图片数据替换掉
      */
-    var rep_img = result.replace(/<img [^>]*src=['"]([^'"]+)[^>]*>/gi, function (match, str) {
+    var rep_img = parse_href.replace(/<img [^>]*src=['"]([^'"]+)[^>]*>/gi, function (match, str) {
         // match值类似于
         //         <img src="https://res.stepfun.com/image_gen/20241125/01936254e2ad7aea9d3d371d9a376469.png?X-Tos-Algorithm=TOS4-HMAC-SHA256&X-Tos-Credential=AKLTYTJhYzZiM2JhZTU2N
         // GMxMGFkNTkwNTk3OTVkJDQ%2F20241125%2Fcn-shanghai%2Ftos%2Frequest&X-Tos-Date=20241125T075910Z&X-Tos-Expires=3600&x-Tos-Signature=3fa70bdbcc1759ab45aaa1b8bd60eef886e55114c32a39f478ee85d4f262804e&x-Tos-SignededH
@@ -52,6 +55,9 @@ export default function formatAnwserString(string) {
         return `<span class="xiaoshuhuitu-image-container" data-src="${img_src}" data-alt="${img_alt}"></span>`;
     });
 
+    /**
+     * 文档引用效果
+     */
     const reg = /<span class="circle" data-list-index="\d"><\/span>/g;
 
     /**