Ver código fonte

Merge branch 'main' of http://gitlab.smartai.com/qi/xiaoshu

张涛 1 ano atrás
pai
commit
f9b0611132

+ 12 - 5
README.md

@@ -10,18 +10,25 @@ This template should help get you started developing with Vue 3 in Vite.
 
 ```sh
 # 安装项目依赖
-$ npm install 
+$ npm install
 $ npm install --legacy-peer-deps
 
 # 本地启动项目
-$ npm run dev
+$ npm run dev # localhost:80
 
 # 构建项目
 $ npm run build
 ```
 
+## 项目须知
+
+1. `dist`目录需要在`git`进行追踪,不必添加到`.gitignore`文件中。
+2. 安装项目依赖使用`npm install --legacy-peer-deps`,仅使用`npm install`命令可能无法启动项目。
+3. 配置文件位于`src/config/settings.json`中,`RAGFlowUrl`需要配置,当前值为`http://192.168.20.119:11080`。如果不配置,则会引起`文档引用`效果出错,图片和 PDF 文档将无法显示。
+
 ## 相关资源
 
-* [Vite配置参考](https://vitejs.dev/config/).
-* [字节跳动前端框架 arco design](https://arco.design/vue/docs/start)
-* [RAGFlow 开源大模型](https://infiniflow.cn/)
+- [Vite 配置参考](https://vitejs.dev/config/).
+- [字节跳动前端框架 arco design](https://arco.design/vue/docs/start)
+- [RAGFlow 开源大模型](https://infiniflow.cn/)
+- [mitt](https://www.npmjs.com/package/mitt)

+ 27 - 0
src/ajax/document.js

@@ -0,0 +1,27 @@
+import config from '../config/settings.json';
+
+/**
+ * 切记 配置文件中的配置项 RAGFlowUrl 不能为空
+ * 否则,文档引用效果将会异常,无法看到图片和文档
+ */
+const server_url = config.RAGFlowUrl;
+
+/**
+ * 获取一个文档的链接
+ * 使用位置1:文档引用效果中的pdf文件展示
+ * @param {string} doc_id 
+ * @returns 
+ */
+export const getDocumentUrl = (doc_id) => {
+    return `${server_url}/v1/document/get/${doc_id}`;
+}
+
+/**
+ * 获取一个图片的链接
+ * 使用位置1:文档引用效果中,浮层左侧小图片展示
+ * @param {string} image_id 
+ * @returns 
+ */
+export const getImageUrl = (image_id) => {
+    return `${server_url}/v1/document/image/${image_id}`
+}

+ 11 - 0
src/assets/images/left-hover.svg

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>左-hover</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="切图" transform="translate(-260, -298)" fill="#00AEA3" fill-rule="nonzero">
+            <g id="左备份" transform="translate(260, 298)">
+                <path d="M13.7071068,4.29289322 C13.3165825,3.90236893 12.6834175,3.90236893 12.2928932,4.29289322 L7.29289322,9.29289322 C6.90236893,9.68341751 6.90236893,10.3165825 7.29289322,10.7071068 L12.2928932,15.7071068 C12.6834175,16.0976311 13.3165825,16.0976311 13.7071068,15.7071068 C14.0976311,15.3165825 14.0976311,14.6834175 13.7071068,14.2928932 L9.415,10 L13.7071068,5.70710678 C14.0675907,5.34662282 14.0953203,4.77939176 13.7902954,4.38710056 L13.7071068,4.29289322 Z" id="路径"></path>
+            </g>
+        </g>
+    </g>
+</svg>

+ 11 - 0
src/assets/images/left-normal.svg

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>左</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="切图" transform="translate(-260, -270)" fill="#333333" fill-rule="nonzero">
+            <g id="左" transform="translate(260, 270)">
+                <path d="M13.7071068,4.29289322 C13.3165825,3.90236893 12.6834175,3.90236893 12.2928932,4.29289322 L7.29289322,9.29289322 C6.90236893,9.68341751 6.90236893,10.3165825 7.29289322,10.7071068 L12.2928932,15.7071068 C12.6834175,16.0976311 13.3165825,16.0976311 13.7071068,15.7071068 C14.0976311,15.3165825 14.0976311,14.6834175 13.7071068,14.2928932 L9.415,10 L13.7071068,5.70710678 C14.0675907,5.34662282 14.0953203,4.77939176 13.7902954,4.38710056 L13.7071068,4.29289322 Z" id="路径"></path>
+            </g>
+        </g>
+    </g>
+</svg>

BIN
src/assets/images/loading.gif


+ 11 - 0
src/assets/images/right-hover.svg

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>右- hover</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="切图" transform="translate(-222, -298)" fill="#00AEA3" fill-rule="nonzero">
+            <g id="右备份" transform="translate(222, 298)">
+                <path d="M7.29289322,4.29289322 C7.68341751,3.90236893 8.31658249,3.90236893 8.70710678,4.29289322 L13.7071068,9.29289322 C14.0976311,9.68341751 14.0976311,10.3165825 13.7071068,10.7071068 L8.70710678,15.7071068 C8.31658249,16.0976311 7.68341751,16.0976311 7.29289322,15.7071068 C6.90236893,15.3165825 6.90236893,14.6834175 7.29289322,14.2928932 L11.585,10 L7.29289322,5.70710678 C6.93240926,5.34662282 6.90467972,4.77939176 7.20970461,4.38710056 L7.29289322,4.29289322 Z" id="路径"></path>
+            </g>
+        </g>
+    </g>
+</svg>

+ 11 - 0
src/assets/images/right-normal.svg

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>右</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="切图" transform="translate(-222, -270)" fill="#333333" fill-rule="nonzero">
+            <g id="右" transform="translate(222, 270)">
+                <path d="M7.29289322,4.29289322 C7.68341751,3.90236893 8.31658249,3.90236893 8.70710678,4.29289322 L13.7071068,9.29289322 C14.0976311,9.68341751 14.0976311,10.3165825 13.7071068,10.7071068 L8.70710678,15.7071068 C8.31658249,16.0976311 7.68341751,16.0976311 7.29289322,15.7071068 C6.90236893,15.3165825 6.90236893,14.6834175 7.29289322,14.2928932 L11.585,10 L7.29289322,5.70710678 C6.93240926,5.34662282 6.90467972,4.77939176 7.20970461,4.38710056 L7.29289322,4.29289322 Z" id="路径"></path>
+            </g>
+        </g>
+    </g>
+</svg>

+ 125 - 24
src/components/DialogWrap.vue

@@ -17,6 +17,7 @@
           {{ suggestion }}<icon-arrow-right />
         </div>
       </div>
+
       <div class="dialog-list" v-if="dialogList.length > 0" :key="randomKey">
         <div :class="['dialog-item', isSetting ? '' : 'dialog-absolute']" v-for="(item, key) in dialogList"
           :key="item.key">
@@ -97,9 +98,11 @@
                 </div>
               </div>
             </section>
+
             <p v-if="type === 'knowledge' && item.answer" style="margin-top: 10px; font-size: 18px; font-weight: bold">
               答案生成结果
             </p>
+
             <p v-if="item.answer" :style="{ 'margin-top': type === 'knowledge' ? '12px' : '0' }">
               <span v-if="type === 'paper'" class="paper-wrap">
                 <img src="@/assets/question/word.svg" />
@@ -115,6 +118,7 @@
           <div class="dialog-footer" v-else>
             <span v-html="getHTML(item.answer)"></span>
           </div>
+
           <!-- 处理来源 -->
           <div v-if="(type === 'knowledge' || type === 'knowledgeQuiz') && item.reference && item.reference.length > 0"
             class="knowledge-wrap">
@@ -123,10 +127,11 @@
               <!-- TODO详情暂时注释 -->
               <!-- <p @click="goKnowledge">查看全部关联 ></p> -->
             </header>
+
             <section>
               <div v-for="(knowledge, knowledgeIndex) in item.reference" :key="knowledgeIndex">
                 <a-popover :title="knowledge.doc_name" position="left">
-                  <div>
+                  <div @click="openDocuementPage(knowledge)">
                     <span>{{ knowledgeIndex + 1 }}</span>
                     <span>{{ knowledge.doc_name }}</span>
                   </div>
@@ -153,8 +158,8 @@
               </div>
             </section>
           </div>
-          <!-- 如果有图片 -->
 
+          <!-- 如果有图片 -->
           <div v-if="item?.image_url">
             <a-image width="500" :src="item?.image_url" />
           </div>
@@ -222,6 +227,8 @@ import { defineComponent, ref, onBeforeUnmount, onMounted, reactive, nextTick, i
 import formatAnwserString from './refference-effect/format-anwser-string';
 import applyRefferencePopover from './refference-effect/apply-refference-popover';
 
+import {getDocumentUrl} from '../ajax/document';
+
 export default defineComponent({
   components: {
     DialogFeedback
@@ -263,13 +270,16 @@ export default defineComponent({
     let dialogIndex = dialogList.length;
     const dialogScroll = ref("");
     const dialogConfig = ref({});
+
     const scrollToEnd = async () => {
       await nextTick();
       if (dialogScroll.value) {
         dialogScroll.value.scrollTop = dialogScroll.value.scrollHeight;
       }
     };
+
     let answerInterval = null;
+
     const stopWriting = () => {
       window.clearInterval(answerInterval);
       emit("set-readonly", false);
@@ -281,28 +291,26 @@ export default defineComponent({
       scrollToEnd();
       dialogIndex += 1;
     };
+
     const addData = (data) => {
       dialogList.splice(dialogIndex, 1, data);
       randomKey.value = Math.random();
       scrollToEnd();
     };
+
     const saveAnswer = (index) => {
       if (!props.readonly) {
         menuList[index].save = !menuList[index].save;
       }
     };
+
     const openCapacity = (index) => {
       if (!props.readonly) {
         menuList[index].open = !menuList[index].open;
       }
     };
-    const openDocument = (index, documentIndex) => {
-      if (menuList[index].documentOpenIndex === documentIndex) {
-        menuList[index].documentOpenIndex = -1;
-      } else {
-        menuList[index].documentOpenIndex = documentIndex;
-      }
-    };
+
+
     const feedbackAnswer = () => {
       if (!props.readonly) {
         showFeedback.value = true;
@@ -319,11 +327,13 @@ export default defineComponent({
 
       }
     };
+
     const copyAnswer = (text) => {
       if (!props.readonly) {
         helper.copyText(text, "复制成功");
       }
     };
+
     const reQuestion = (text) => {
       if (!props.readonly) {
         setTimeout(() => {
@@ -331,9 +341,29 @@ export default defineComponent({
         }, 200);
       }
     };
+
+    const openDocument = (index, documentIndex) => {
+      if (menuList[index].documentOpenIndex === documentIndex) {
+        menuList[index].documentOpenIndex = -1;
+      } else {
+        menuList[index].documentOpenIndex = documentIndex;
+      }
+    };
     const goKnowledge = () => {
-      emit("go-document");
+      // emit("go-document");
     };
+    // 点击文档,打开新页面
+    const openDocuementPage = (knowledge) => {
+      const doc_name = knowledge.doc_name;
+      const doc_id =  knowledge.id;
+
+      // const pdf_url = `http://192.168.20.119:11080/v1/document/get/${doc_id}`;
+      const pdf_url = getDocumentUrl(doc_id);
+
+      window.open(pdf_url);
+    }
+
+
     const addLoading = (data) => {
       const mockData = DOCUMENT_RESULT;
       data.capacity = {
@@ -388,6 +418,7 @@ export default defineComponent({
       helper.webSocket.close();
     });
 
+
     onMounted(() => {
       scrollToEnd();
       /**
@@ -395,20 +426,70 @@ export default defineComponent({
        * 答案中 引用图标(感叹号)点击时,浮层显示引用内容
        */
       applyRefferencePopover(props.list);
-      // 如果是智能数据,则拿缓存里的数据开启对话
-      if (helper.read("sendData") && helper.read("sendData").agent_id == 'basic_excel_talk') {
-        const data = helper.read("sendData");
-        helper.webSocket = ajax.message.create(helper.read("token"), data.agent_id, data.chat_id, props.type);
-        // 创建对话之后清空缓存
-        helper.write('sendData', {});
-      } else {
-        helper.webSocket = ajax.message.create(helper.read("token"), props.agentId, props.dialogId, props.type);
+
+
+      // socket 消息队列
+      // TODO: 只允许保存一条消息
+      let msg_list = [];
+
+      function createSocket(){
+        const token = helper.read("token");
+        // 如果是智能数据,则拿缓存里的数据开启对话
+        if (helper.read("sendData") && helper.read("sendData").agent_id == 'basic_excel_talk') {
+          const data = helper.read("sendData");
+
+          helper.webSocket = ajax.message.create(token, data.agent_id, data.chat_id, props.type);
+
+          // 创建对话之后清空缓存
+          helper.write('sendData', {});
+        } else {
+          helper.webSocket = ajax.message.create(token, props.agentId, props.dialogId, props.type);
+        }
+
+        // 监听socket开启事件
+        // 当socket开启后,遍历消息,并发送
+        helper.webSocket.onopen = function(event){
+          msg_list.forEach(item => {
+            helper.webSocket.send(item);
+          });
+
+          // 所有消息发送完成后,清空消息队列
+          msg_list = [];
+        }
       }
 
+      // 发送socket数据
+      function sendSocketMsg(dt){
+        // 判断socket是否开启
+        // 如果开启,则直接发送,如果没有开启,则将数据推入消息队列 
+        if(helper.webSocket.readyState === 0){
+          msg_list.push(dt);
+        }
+        if(helper.webSocket.readyState === 1){
+          helper.webSocket.send(dt);
+        }
+        if(helper.webSocket.readyState === 2){
+          // 正在关闭
+          console.log('web socket正在关闭');
+        }
+        if(helper.webSocket.readyState === 3){
+          msg_list.push(dt);
+
+          // 已关闭: 重新开启socket
+          createSocket();
+        }
+      }
+
+      // 创建socket链接
+      createSocket();
+
       helper.webSocket.onmessage = (event) => {
         const { type, reference, step_message, files } = JSON.parse(event.data);
+
         let { message } = JSON.parse(event.data);
+
         if (!loadingData.value) return;
+
         if (type === "stream") {
           // 没解析完就一直loading
           if (step_message == '文件上传完成,开始解析') {
@@ -423,9 +504,11 @@ export default defineComponent({
             addData(loadingData.value);
           } else {
             answerData.value += step_message ? getHTML(step_message) + "</br>" : message
+
             if (answerData.value && !isLoaded.value) {
               answerLoaded();
             }
+
             addAnswer(loadingData.value);
           }
         }
@@ -458,17 +541,20 @@ export default defineComponent({
             if (image_url) {
               imageUrl.value = `http://${window.location.hostname}${port}${image_url}`
             }
+
             if (excel_url) {
               const file = {
                 file_type: 'excel',
                 file_url: `http://${window.location.hostname}${port}${excel_url}`,
                 file_name: `${excel_name}.xlsx` || '点击下载',
               }
+
               answerFile.value = file
             }
+
             if (code) answerCode.value = getHTML(code);
-            if (sql) answerSql.value = getHTML(sql);
 
+            if (sql) answerSql.value = getHTML(sql);
           }
           if (props.type == 'question') {
             const { file_url, filename, excel_name } = JSON.parse(event.data);
@@ -486,11 +572,10 @@ export default defineComponent({
           if (answerData.value && !isLoaded.value) {
             answerLoaded();
           }
+
           addAnswer(loadingData.value);
         }
 
-
-
         if (type === "close") {
           if (!answerData.value) {
             answerLoaded();
@@ -502,6 +587,11 @@ export default defineComponent({
           isLoaded.value = false;
         }
       };
+
+      /**
+       * 所有数据的发送,统一走到这里
+       * 通过websocket进行发送
+       */
       helper.$bus.on("send-message", (value) => {
         dialogConfig.value = helper.dialogConfig;
         const question = props.type === "report" ? value.inputs.report_name : value;
@@ -513,22 +603,30 @@ export default defineComponent({
             : {
               message: value
             };
+
         if (props.type === 'doc') {
           sendData.doc_ids = [helper.read('docId')];
         }
-        helper.webSocket.send(JSON.stringify(sendData));
+
+        const dt = JSON.stringify(sendData);
+
+        // 发送socket数据
+        sendSocketMsg(dt)
       });
+
       helper.$bus.on("clear-message", () => {
         dialogList.splice(0);
       });
+
       helper.$bus.on("stop-writing", () => {
         stopWriting();
       });
     });
+
     return {
       goKnowledge,
       randomKey,
-      openDocument,
+      
       showFeedback,
       copyAnswer,
       dialogList,
@@ -546,7 +644,10 @@ export default defineComponent({
       knowledgeList,
       dialogConfig,
       userName,
-      downloadFile
+      downloadFile,
+
+      openDocument,
+      openDocuementPage
     };
   }
 });

+ 142 - 0
src/components/MessagePicUpload.vue

@@ -0,0 +1,142 @@
+<template>
+    <div class="message-input-wrap">
+        <div class="message-input-border">
+            <div class="message-input-clear" @click="clearMessage">
+                <div :class="[readonly ? 'disabled' : '']">
+                    <img src="@/assets/icons/iconPark-clear.svg" width="15" />
+                </div>
+            </div>
+            <a-textarea @keydown.enter="handleEnter" :max-length="maxLength" v-model="value"
+                :show-word-limit="value.length > maxLength / 2" :auto-size="{ maxRows: 6 }" :disabled="readonly"
+                placeholder="输入您想了解的内容,Shift+Enter换行" :style="{
+                    flex: 1,
+                    borderRadius: '12px',
+                    marginTop: '20px',
+                    padding: '0px 30px 0 16px',
+                    background: 'transparent',
+                    border: '1px solid #e3e3e3',
+                    minHeight: '120px',
+                    paddingBottom: `${value.length > maxLength / 2 ? '20px' : ''}`
+                }">
+            </a-textarea>
+        </div>
+        <div :class="['message-button', 'message-button-send', value.length > 0 && !readonly ? '' : 'disabled']"
+            @click="sendMessage">
+            <icon-send size="30" />
+        </div>
+    </div>
+</template>
+<script>
+import { defineComponent, ref, inject, onMounted, onBeforeUnmount } from "vue";
+export default defineComponent({
+    components: {},
+    props: ["readonly", "eventName"],
+    setup(props) {
+        const { helper } = inject("$global");
+        const value = ref("");
+        const maxLength = ref(6000);
+        const sendMessage = () => {
+            if (value.value.length > 0 && !props.readonly) {
+                const eventName = props.eventName || "send-message";
+                helper.$bus.emit(eventName, value.value);
+                value.value = "";
+            }
+        };
+        const clearMessage = () => {
+            if (!props.readonly) {
+                helper.$bus.emit("clear-message");
+            }
+        };
+        const handleEnter = (e) => {
+            if (!e.shiftKey) {
+                e.preventDefault();
+                sendMessage();
+            }
+        };
+        onMounted(() => {
+            helper.$bus.on("set-input", (data) => {
+                value.value = data;
+            });
+        });
+        onBeforeUnmount(() => {
+            helper.$bus.off("set-input");
+        });
+        return {
+            value,
+            maxLength,
+            readonly: props.readonly,
+            sendMessage,
+            clearMessage,
+            handleEnter
+        };
+    }
+});
+</script>
+<style scoped>
+.message-input-border {
+    display: flex;
+}
+
+.message-input-clear {
+    width: 30px;
+    height: 100%;
+    margin-right: 8px;
+    display: none;
+
+    div {
+        width: 30px;
+        height: 30px;
+        border-radius: 15px;
+        background-color: rgba(64, 95, 234, 0.05);
+        position: absolute;
+        bottom: 0;
+        cursor: pointer;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+
+        &.disabled {
+            cursor: not-allowed;
+        }
+
+        &:hover {
+            background-color: rgba(64, 95, 234, 0.1);
+        }
+    }
+}
+
+.message-input-wrap {
+    position: relative;
+    margin-bottom: 20px;
+}
+
+.message-button {
+    position: absolute;
+    width: 30px;
+    height: 40px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+    bottom: 5px;
+    right: 5px;
+    z-index: 2;
+
+    &.disabled {
+        opacity: 0.5;
+        cursor: not-allowed;
+    }
+}
+
+.message-button-upload {
+    left: 38px;
+}
+
+.message-button-send {
+    color: rgb(64, 95, 234, 0.8);
+
+    &:hover {
+        color: rgb(64, 95, 234);
+    }
+}
+</style>

+ 573 - 0
src/components/PictureCards.vue

@@ -0,0 +1,573 @@
+<template>
+  <main>
+    <section>
+      <div v-for="(item, key) in data" :key="key" :style="{ background: item.background }">
+        <img :src="getAssetURL(item.img)" />
+        <div :style="{ color: item.color }">
+          {{ item.title }}
+          <span>{{ item.content }}</span>
+        </div>
+      </div>
+    </section>
+
+    <AnwserPictureResult :image_list="image_list"/>
+    <AnwserPictureLoading />
+    
+    <footer v-if="hasUpload">
+      <a-popover :popup-visible="showUpload" position="tl" @popup-visible-change="() => {
+        showDeleteTooltip = false;
+      }
+        ">
+        <div :class="{ 'text-hide': showUpload }">
+          <div @click="() => {
+            showUpload = true;
+          }
+            ">
+            <img src="@/assets/document/6.svg" class="img-normal" v-if="!showUpload" />
+            <img src="@/assets/document/7.svg" class="img-hover" v-if="!showUpload" />
+            <img src="@/assets/document/8.svg" class="img-active" v-if="showUpload" />
+          </div>
+          <p>已选择 {{ fileList.length }} 个图片</p>
+        </div>
+        <template #content>
+          <div :class="{ 'upload-wrap': true, 'upload-wrap-narrow': uploadFile }">
+            <header>
+              <p>{{ uploadFile ? "上传图片" : "上传图片" }}</p>
+              <div @click="() => {
+                showUpload = false;
+              }
+                ">
+                <icon-close size="large" />
+              </div>
+            </header>
+
+            <div>
+              <section>
+                <a-upload draggable :multiple="!uploadFile" :limit="50" :show-file-list="false"
+                  @before-upload="onBeforeUpload" @exceed-limit="onExceedLimit" @before-remove="onBeforeRemove"
+                  :custom-request="customRequest" :show-upload-button="{ showOnExceedLimit: true }"
+                  accept=".png,.jpg">
+                  <template #upload-button>
+                    <div class="upload-main">
+                      <p>拖拽图片到这里,或<span>点此上传</span></p>
+                      <span>
+                        支持上传 .png, .jpg,大小不超过10M
+                      </span>
+                    </div>
+                  </template>
+                </a-upload>
+              </section>
+
+              <!-- <section v-else>
+                <a-upload draggable :multiple="!uploadFile" :limit="50" :show-file-list="false"
+                  @before-upload="onBeforeUpload" @exceed-limit="onExceedLimit" @before-remove="onBeforeRemove"
+                  :custom-request="customRequest" :show-upload-button="{ showOnExceedLimit: true }" accept="*">
+                  <template #upload-button>
+                    <div class="upload-main">
+                      <p>拖拽图片到这里,或<span>点此上传</span></p>
+                      <span>
+                        支持上传{{ uploadFile ? ".png,.jpg" : ".xlsx‌文档" }},大小不超过10M
+                      </span>
+                    </div>
+                  </template>
+                </a-upload>
+              </section> -->
+
+              <div v-if="tableData.length > 0">
+                <p>
+                  <span>已选择图片列表</span>
+                  <span>共{{ tableData.length }}个</span>
+                </p>
+                <div>
+                  <a-table :pagination="false" row-key="name" :columns="columns" :data="tableData"
+                    :row-selection="rowSelection" v-model:selectedKeys="selectedKeys">
+                    <template #methods="{ record }">
+                      <a-tooltip content="下载">
+                        <div class="method-button" @click="downloadDocument(record)">
+                          <icon-cloud-download />
+                        </div>
+                      </a-tooltip>
+                      <a-popover :popup-visible="showDeleteTooltip && deleteId === record.key">
+                        <a-tooltip content="删除">
+                          <div class="method-button" @click="handleDeleteButton(record.key)">
+                            <icon-delete />
+                          </div>
+                        </a-tooltip>
+                        <template #content>
+                          <div>
+                            <p>删除后无法恢复,您是否删除?</p>
+                            <div class="delete-buttons">
+                              <div>
+                                <a-button size="mini" type="primary" style="margin-right: 10px"
+                                  @click="deleteDocument">确认</a-button>
+                                <a-button size="mini" type="outline" @click="() => {
+                                  showDeleteTooltip = false;
+                                }
+                                  ">取消</a-button>
+                              </div>
+                            </div>
+                          </div>
+                        </template>
+                      </a-popover>
+                    </template>
+                  </a-table>
+                </div>
+              </div>
+            </div>
+          </div>
+
+          <div class="bottom-buttons">
+            <div>
+              <a-button type="outline" style="margin-right: 10px" @click="selectFiles(false)">取消</a-button>
+              <a-button :loading="buttonLoading" type="primary" @click="selectFiles(true, 'paper')">去{{
+                uploadFile ? '提问'
+                  : '提问'
+              }}({{ fileList.length
+                }})</a-button>
+            </div>
+          </div>
+        </template>
+      </a-popover>
+      <!-- <section v-if="hasSlider">
+        <p>出题随机性</p>
+        <a-slider :default-value="50" :style="{ width: '100px' }" />
+      </section> -->
+    </footer>
+  </main>
+</template>
+<script>
+import dayjs from "dayjs";
+import { defineComponent, ref, inject, reactive } from "vue";
+
+import AnwserPictureResult from './anwser-pictures/AnwserPictureResult.vue';
+import AnwserPictureLoading from './anwser-pictures/AnwserPictureLoading.vue';
+
+export default defineComponent({
+  props: ["data", "hasUpload", "hasSlider", "uploadFile", "isQuestion", "agentId", "agengtName"],
+  components: {
+    AnwserPictureResult,
+    AnwserPictureLoading
+  },
+  setup(props) {
+    const showUpload = ref(false);
+    const { helper, ajax } = inject("$global");
+
+    const maxSize = 30 * 1024 * 1024;
+    let fileList = ref([]);
+    const tableData = reactive([]);
+    let formData = new FormData();
+    let inputData = ref([]);
+    const showDeleteTooltip = ref(false);
+    const deleteId = ref("");
+    const buttonLoading = ref(false);
+    const selectFiles = async (isSelect, type) => {
+      if (isSelect) {
+        if (props.uploadFile) {
+          const data = await ajax.message.createDialog(props.agentId);
+          fileList.value.forEach(file => {
+            formData.append('file', file)
+          });
+          buttonLoading.value = true;
+          const result = await ajax.file.uploadFile(props.agentId, data.chat_id, formData);
+          showUpload.value = false;
+          helper.dialogConfig = {
+            isQuestion: props.isQuestion,
+            selectedKeys: selectedKeys.value,
+            type
+          };
+          buttonLoading.value = false;
+          // if (result) {
+          //     helper.write('docId', result.doc_ids[0]);
+          // }
+
+          // 以下为判断是毕生还是regflow
+          // TODO 判断是毕生还是regflow
+          // 以下为毕生
+          inputData.value.push({
+            name: result.file_name,
+            file_path: result.file_path,
+            value: ""
+          });
+
+          const sendData = {
+            chatHistory: [],
+            chat_id: data.chat_id,
+            agent_id: props.agentId,
+            name: "小数绘图",
+            description: "小数绘图",
+            inputs: {
+              report_name: inputData.value.map(item => item.name).join(', '),
+              id: "Report-PRcYL",
+              data: inputData.value
+            }
+          };
+
+          if (sendData) {
+            helper.write('sendData', sendData);
+          }
+        } else {
+          fileList.value.forEach(file => {
+            formData.append('files', file)
+          });
+
+          await ajax.file.uploadExcels(formData);
+          helper.$bus.emit("open-dialog", '合并Excel');
+        }
+      }
+
+    };
+
+    const handleDeleteButton = (id) => {
+      showDeleteTooltip.value = true;
+      deleteId.value = id;
+    };
+
+    const onBeforeUpload = (file) => {
+      if (file.size > maxSize) {
+        helper.message("error", `上传文件大小不能超过10M`);
+        return false;
+      }
+      return true;
+    };
+
+    const onExceedLimit = () => {
+      helper.message("error", `最多上传50个文件`);
+    };
+
+    const customRequest = async (option) => {
+      const { onSuccess, fileItem } = option;
+      const { file } = fileItem;
+      fileList.value.push(file);
+      tableData.push({
+        key: file.uid,
+        name: file.name,
+        type: file.type,
+        size: helper.formatSize(file.size),
+        time: dayjs(file.lastModified).format("YYYY-MM-DD HH:mm:ss")
+      });
+      onSuccess();
+    };
+
+    const onBeforeRemove = (data) => {
+      console.log('data', data)
+      fileList.value = fileList.value.filter((item) => item.id !== data.uid);
+      return true;
+    };
+    const selectedKeys = ref([]);
+
+    const rowSelection = reactive({
+      type: "checkbox",
+      showCheckedAll: true,
+      onlyCurrent: false
+    });
+
+    const columns = [
+      {
+        title: "文件名称",
+        dataIndex: "name",
+        ellipsis: true,
+        tooltip: true,
+        width: 150
+      },
+      {
+        title: "类型",
+        dataIndex: "type",
+        ellipsis: true,
+        tooltip: true,
+        width: 150
+      },
+      {
+        title: "大小",
+        dataIndex: "size"
+      },
+      {
+        title: "上传时间",
+        dataIndex: "time",
+        ellipsis: true,
+        tooltip: true,
+        width: 150
+      },
+      {
+        title: "操作",
+        slotName: "methods"
+      }
+    ];
+
+    const downloadDocument = (record) => { };
+
+    const deleteDocument = () => {
+      const index = tableData.findIndex(item => item.key === deleteId.value);
+      tableData.splice(index, 1);
+      fileList.value = fileList.value.filter(item => item.uid !== deleteId.value);
+    };
+
+
+    const image_list = [
+      'https://s3.harix.iamidata.com/sd-txt2img-ningbo/20241121_4173677d-b021-4b3a-9535-b7e4de17cded?AWSAccessKeyId=mjf0vn4dlj76z7ph&Expires=2147483647&Signature=kBHLFhQKRUd%2F%2B1hnw0k5DZ8VuA4%3D',
+      'https://s3.harix.iamidata.com/sd-txt2img-ningbo/20241121_f0c4a724-ed43-4950-ade0-60190aa8eda4?AWSAccessKeyId=mjf0vn4dlj76z7ph&Expires=2147483647&Signature=Vd8Ja5X3W8qJFZl30NE8d%2B5GW40%3D',
+      'https://s3.harix.iamidata.com/sd-txt2img-ningbo/20241121_a969df4c-5b6f-4577-827d-f7459e1a1c69?AWSAccessKeyId=mjf0vn4dlj76z7ph&Expires=2147483647&Signature=CHVinI9kJWnToLXWZ5BHdj54nLM%3D'
+    ]
+
+
+    return {
+      agengtName: props.agengtName,
+      buttonLoading,
+      fileList,
+      selectedKeys,
+      getAssetURL: helper.getAssetURL,
+      showUpload,
+      onBeforeUpload,
+      onExceedLimit,
+      onBeforeRemove,
+      customRequest,
+      columns,
+      tableData,
+      rowSelection,
+      downloadDocument,
+      deleteDocument,
+      showDeleteTooltip,
+      deleteId,
+      handleDeleteButton,
+      selectFiles,
+
+      image_list
+    };
+  }
+});
+</script>
+<style lang="scss" scoped>
+.bottom-buttons {
+  display: flex;
+  justify-content: right;
+
+  >div {
+    display: flex;
+  }
+}
+
+.delete-buttons {
+  margin-top: 10px;
+  display: flex;
+  justify-content: center;
+}
+
+.method-button {
+  display: inline-block;
+  padding: 4px 8px;
+  border-radius: 5px;
+  cursor: pointer;
+
+  &:hover {
+    background: rgba(150, 171, 185, 0.2);
+  }
+}
+
+.upload-main {
+  width: 800px;
+  height: 109px;
+  background: url("@/assets/document/upload.svg");
+  margin: 0 auto;
+  padding: 24px 0px 24px 277px;
+  background-size: 100%;
+
+  >p {
+    color: #000;
+    font-weight: bold;
+    margin-bottom: 5px;
+    margin-top: 10px;
+
+    >span {
+      color: #00aea3;
+      text-decoration: underline;
+    }
+  }
+
+  >span {
+    font-size: 13px;
+    color: #7f7f7f;
+  }
+}
+
+main {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+
+  >section {
+    overflow-y: auto;
+    display: flex;
+    flex-wrap: wrap;
+    gap: 20px;
+
+    &.section-full {
+      flex: 1;
+    }
+
+    >div {
+      width: 382px;
+      height: 120px;
+      padding: 20px 24px;
+      border-radius: 8px;
+      display: flex;
+
+      >img {
+        width: 38px;
+        height: 38px;
+        margin-right: 12px;
+      }
+
+      >div {
+        font-size: 18px;
+        font-weight: bold;
+
+        >span {
+          font-size: 15px;
+          font-weight: 400;
+          color: #797d7f;
+          margin-top: 8px;
+          overflow: hidden;
+          text-overflow: ellipsis;
+          display: -webkit-box;
+          -webkit-line-clamp: 2;
+          -webkit-box-orient: vertical;
+          line-height: 1.4;
+        }
+      }
+    }
+  }
+
+  >footer {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-top: auto;
+
+    >div {
+      display: inline-flex;
+      align-items: center;
+      gap: 8px;
+      height: 32px;
+      border-radius: 21px;
+      border: 1px solid #e3e3e3;
+      white-space: nowrap;
+      overflow: hidden;
+      transition-duration: 0.3s;
+      width: 250px;
+
+      &.text-hide {
+        width: 110px;
+        overflow: hidden;
+      }
+
+      &.text-narrow {
+        width: 100px;
+        overflow: hidden;
+
+        >div>img {
+          width: 100px;
+        }
+      }
+
+      >p {
+        overflow: hidden;
+        white-space: nowrap;
+      }
+
+      >div {
+        cursor: pointer;
+        width: 110px;
+        height: 32px;
+
+        .img-normal {
+          display: block;
+        }
+
+        .img-hover {
+          display: none;
+        }
+
+        &:hover {
+          .img-normal {
+            display: none;
+          }
+
+          .img-hover {
+            display: block;
+          }
+        }
+
+        >img {
+          width: 110px;
+          height: 32px;
+        }
+      }
+    }
+
+    >section {
+      display: flex;
+      align-items: center;
+      background: rgba(150, 171, 185, 0.2);
+      border-radius: 20px;
+      padding: 5px 10px;
+
+      >p {
+        margin-right: 5px;
+      }
+    }
+  }
+}
+
+.upload-wrap {
+  width: 800px;
+  height: 300px;
+  display: flex;
+  flex-direction: column;
+  padding-bottom: 30px;
+
+  &.upload-wrap-narrow {
+    height: auto;
+  }
+
+  >header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 13px;
+
+    >p {
+      font-size: 18px;
+      font-weight: bold;
+    }
+
+    >div {
+      width: 38px;
+      height: 38px;
+      border-radius: 6px;
+      cursor: pointer;
+      justify-content: center;
+      align-items: center;
+      display: flex;
+
+      &:hover {
+        background: rgba(0, 0, 0, 0.05);
+      }
+    }
+  }
+
+  >div {
+    flex: 1;
+    overflow-y: auto;
+
+    >div {
+      >p {
+        margin: 20px 0;
+        color: #7f7f7f;
+        font-size: 13px;
+        display: flex;
+        justify-content: space-between;
+      }
+    }
+  }
+}
+</style>

+ 108 - 0
src/components/anwser-pictures/AnwserInfo.vue

@@ -0,0 +1,108 @@
+<template>
+    <div class="anwser-pictures-info">
+        <div class="anwser-pictures-index-container">
+            <span class="pic-arrow-btn pre" @click="handlePre">
+                <img :src="left_normal_icon" v-if="!can_go_pre"/>
+                <img :src="left_hover_icon" v-if="can_go_pre"/>
+            </span>
+
+            <span><span class="current-index">{{current}}</span> / {{total}}</span>
+
+            <span class="pic-arrow-btn next" @click="handleNext">
+                <img :src="right_hover_icon" v-if="can_go_next"/>
+                <img :src="right_normal_icon" v-if="!can_go_next"/>
+            </span>
+
+            <span>回答已停止生成</span>
+        </div>
+    </div>
+</template>
+
+<script setup>
+import {ref, computed} from 'vue';
+
+import left_hover_icon from '../../assets/images/left-hover.svg'
+import left_normal_icon from '../../assets/images/left-normal.svg'
+import right_hover_icon from '../../assets/images/right-hover.svg'
+import right_normal_icon from '../../assets/images/right-normal.svg'
+
+const props = defineProps({
+    current: Number, // 当前的序号
+    total: Number // 回答的总数
+});
+
+const emit = defineEmits('pre', 'next');
+
+const can_go_pre = computed(() => {
+    const pre_index = props.current - 1;
+
+    return pre_index >= 1;
+});
+const can_go_next = computed(() => {
+    const next_index = props.current + 1;
+
+    return next_index <= props.total;
+});
+
+/**
+ * 前一张图片
+ */
+const handlePre = () => {
+    const pre_index = props.current - 1;
+
+    if(pre_index < 1){
+        // 已经是第0张,前面没有了
+        return;
+    }else{
+        emit('pre', pre_index);
+    }
+}
+
+/**
+ * 后一张图片
+ */
+const handleNext = () => {
+    const next_index = props.current + 1;
+
+    if(next_index > props.total){
+        console.log('已经是最后一张了');
+        return;
+    }else{
+        emit('next', next_index);
+    }
+}
+</script>
+
+<style scoped>
+.anwser-pictures-container{
+
+}
+
+.anwser-pictures-images{
+    width: 450px;
+    height: 450px;
+    border-radius: 15px;
+    overflow: hidden;
+}
+
+.anwser-pictures-index-container{
+    display: flex;
+    align-items: center;
+}
+
+.pic-arrow-btn{
+    cursor: pointer;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+}
+
+.current-index{
+    font-weight: bold;
+}
+
+.anwser-pictures-info{
+    margin-top: 15px;
+    font-size: 16px;
+}
+</style>

+ 20 - 0
src/components/anwser-pictures/AnwserPictureLoading.vue

@@ -0,0 +1,20 @@
+<template>
+    <div class="anwser-pictures-loading">
+        <img :src="image_loading" />
+    </div>
+</template>
+<script setup>
+import image_loading from '../../assets/images/loading.gif'
+
+</script>
+
+<style scoped>
+.anwser-pictures-loading{
+    width: 35px;
+    height: 35px;
+}
+
+.anwser-pictures-loading img{
+    width: 100%;
+}
+</style>

+ 122 - 0
src/components/anwser-pictures/AnwserPictureResult.vue

@@ -0,0 +1,122 @@
+<template>
+    <div class="anwser-pictures-container">
+        <div class="anwser-pictures-images">
+            <a-carousel
+                :style="{
+                    width: '450px',
+                    height: '450px',
+                    borderRadius: '15px'
+                }"
+                :auto-play="false"
+                :current="cur_index"
+                animation-name="fade"
+                show-arrow="never"
+                indicator-type="never"
+            >
+                <a-carousel-item v-for="image in image_list" :key="image">
+                    <a-image
+                        data-flag="a-image-container"
+                        :src="image"
+                        :style="{
+                            width: '100%',
+                        }"
+                        :hide-footer="true"
+                    />
+                </a-carousel-item>
+            </a-carousel>
+        </div>
+
+        <AnwserInfo  
+            v-if="!is_only_one" 
+            :current="cur_index" 
+            :total="image_total" 
+            @pre="handlePre" 
+            @next="handleNext" 
+        />
+    </div>
+</template>
+<script setup>
+import {ref, computed} from 'vue';
+
+import AnwserInfo from './AnwserInfo.vue';
+
+import left_hover_icon from '../../assets/images/left-hover.svg'
+import left_normal_icon from '../../assets/images/left-normal.svg'
+import right_hover_icon from '../../assets/images/right-hover.svg'
+import right_normal_icon from '../../assets/images/right-normal.svg'
+
+const cur_index = ref(1);
+
+const props = defineProps({
+    image_list: Array
+});
+
+const image_total = props.image_list && props.image_list.length || 0;
+
+// 是否只有一张图片
+const is_only_one = computed(() => {
+    return image_total === 0;
+});
+
+/**
+ * 前一张图片
+ */
+const handlePre = () => {
+    const pre_index = cur_index.value - 1;
+
+    if(pre_index < 1){
+        // 已经是第0张,前面没有了
+        return;
+    }else{
+        cur_index.value = pre_index;
+    }
+}
+
+/**
+ * 后一张图片
+ */
+const handleNext = () => {
+    const next_index = cur_index.value + 1;
+
+    if(next_index > image_total){
+        console.log('已经是最后一张了');
+        return;
+    }else{
+        cur_index.value = next_index;
+    }
+}
+</script>
+
+<style scoped>
+.anwser-pictures-container{
+
+}
+
+.anwser-pictures-images{
+    width: 450px;
+    height: 450px;
+    border-radius: 15px;
+    overflow: hidden;
+}
+
+.anwser-pictures-index-container{
+    display: flex;
+    align-items: center;
+}
+
+.pic-arrow-btn{
+    cursor: pointer;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+}
+
+.current-index{
+    font-weight: bold;
+}
+
+.anwser-pictures-info{
+    margin-top: 15px;
+    font-size: 16px;
+}
+</style>

+ 10 - 4
src/components/file/FileNameDisplay.vue

@@ -1,12 +1,10 @@
 <template>
-    <div class="file-name-display">
+    <div class="file-name-display" :class="{'file-name-pdf': is_pdf, 'file-name-others': !is_pdf}">
         <span class="file-name-icon"><img :src="img_src"/></span>
         <span class="file-name-label">{{file_name}}</span>
     </div>
 </template>
 <script setup>
-import { defineComponent, inject, ref, useTemplateRef, watchEffect, onMounted} from "vue";
-
 import icon_excel from '../../assets/files/excel.svg';
 import icon_word from '../../assets/files/word-01.svg';
 import icon_pdf from '../../assets/files/pdf.44344347.svg';
@@ -27,8 +25,11 @@ const file_endfix = (function(){
     return str;
 })();
 
+const is_pdf = file_endfix === 'pdf';
+
 const icon_map = {
     docx: icon_word,
+    doc: icon_word,
     pdf: icon_pdf,
 }
 
@@ -37,11 +38,16 @@ const img_src = icon_map[file_endfix];
 </script>
 <style scoped>
 .file-name-display{
-    cursor: pointer;
     display: flex;
     justify-content: flex-start;
     align-items: center;
 }
+.file-name-pdf{
+    cursor: pointer;
+}
+.file-name-others{
+    cursor: default;
+}
 
 .file-name-icon{
     display: inline-block;

+ 22 - 1
src/components/pdf-viewer/PDFViewer.vue

@@ -18,8 +18,29 @@ import PDFJS from './pdfjs';
  * 此路径指向PDF文件
  */
 const props = defineProps({
+    /**
+     * PDF文档的链接
+     */
     url: String,
+    /**
+     * 想要展示的PDF文档的宽度
+     * 是一个数字
+     * 例如:800,表示希望PDF文档的每一页展示为800px宽度
+     */
     doc_width: Number,
+    /**
+     * PDF文档的高亮区域,是一个数组的数组
+     * 例如:[[7, 200, 500, 100, 300], [7, 300, 600, 400, 420]]
+     * 数组中的每一项表示一个高亮区域,数组有几个元素,表示有几个高亮区域
+     * 以示例数据中的第一个高亮区域 [7, 200, 500, 100, 300] 为例
+     * 7 表示高亮区域所处的页码
+     * 200 表示 x1,距离第7页左侧的距离
+     * 500 表示 x2, 距离第7页左侧的距离
+     * 100 表示 y1, 距离第7页顶部的距离
+     * 300 表示 y2, 距离第7页顶部的距离
+     * 最终形成的四个坐标点为:(x1,y1) (x2,y1) (x1,y2) (x2,y2)
+     * 通过这四个坐标点,围合成一个矩形区域
+     */
     positions: Array,
 });
 
@@ -148,7 +169,7 @@ onMounted(() => {
                 });
             }
 
-            box.value.appendChild(canvas);
+            box.value && box.value.appendChild(canvas);
         }
 
         // 所有页面都加载完毕,结束加载状态

+ 2 - 2
src/components/PdfReader.vue → src/components/pdf-viewer/PdfReader.vue

@@ -2,8 +2,8 @@
   <div class="doc-viewer" ref="elementRef"></div>
 </template>
 <script>
-import '../../public/pdf/image-viewer.umd'
-import '../../public/pdf/style.css'
+import '../../../public/pdf/image-viewer.umd'
+import '../../../public/pdf/style.css'
 import { defineComponent, onMounted, ref } from 'vue'
 export default defineComponent({
   components: {

+ 7 - 0
src/components/pdf-viewer/README.md

@@ -0,0 +1,7 @@
+# PDF 阅读器
+
+此目录下为 pdf web 端预览解决方案。
+
+`PDFReader.vue`组件为旧版,位于`components/PDFReader.vue`,但是未经任何组件依赖,也就是说此组件未被使用,于是我将其迁移到此目录下。
+
+`PDFViewer.vue`组件是为了实现`文档引用效果`而写。

+ 40 - 0
src/components/refference-effect/AsideImage.vue

@@ -0,0 +1,40 @@
+<template>
+    <a-popover>
+        <div class="image-aisde-btn-container">
+            <img :src="src" class="image-aside-btn" />
+        </div>
+        
+        <template #content>
+            <img :src="src" class="image-large-view" />
+        </template>
+    </a-popover>
+</template>
+<script setup>
+import {ref, computed} from 'vue';
+
+const props = defineProps({
+    src: String
+});
+</script>
+
+<style scoped>
+.image-aisde-btn-container{
+    width: 120px;
+    max-height: 120px;
+    flex-shrink: 0;
+    overflow: hidden;
+
+    margin-right: 5px;
+}
+
+.image-aside-btn{
+    width: 120px;
+    max-height: 120px;
+
+}
+
+.image-large-view{
+    max-width: 540px;
+    max-height: 540px;
+}
+</style>

+ 26 - 7
src/components/refference-effect/RefferenceFlag.vue

@@ -5,11 +5,18 @@
 
             <template #title></template>
             <template #content>
-                <div class="ref-text-content">
-                    {{text}}
-                </div>
+                <div class="refference-content">
+                    <AsideImage :src="img_url"/>
+
+                    <div class="refference-description">
+                        <div class="ref-text-content">
+                            {{text}}
+                        </div>
 
-                <FileNameDisplay @click="handleClick" :file_name="doc_name" :file_id="doc_id" />
+                        <FileNameDisplay @click="handleClick" :file_name="doc_name" :file_id="doc_id" />
+                    </div>
+                </div>
+                
             </template>
         </a-popover>
 
@@ -27,8 +34,11 @@ import { defineComponent, inject, ref, useTemplateRef } from "vue";
 
 import PDFViewer from '../pdf-viewer/PDFViewer.vue';
 
+import AsideImage from './AsideImage.vue';
 import FileNameDisplay from '../file/FileNameDisplay.vue';
 
+import {getDocumentUrl, getImageUrl} from '../../ajax/document';
+
 /**
  * 此页面可能用到的文档
  * @see https://arco.design/vue/component/popover
@@ -36,17 +46,17 @@ import FileNameDisplay from '../file/FileNameDisplay.vue';
 
 const props = defineProps({
     img: String, // 图片链接
+    image_id: String,
     text: String, // 文本地址
     doc_id: String,
     doc_name: String,
     positions: Array
 });
 
-// const DOC_WIDTH = 620;
 const DOC_WIDTH = window.innerWidth/2;
 
-// http://192.168.20.119:11080/v1/document/get/
-const pdf_url = ref(`http://192.168.20.119:11080/v1/document/get/${props.doc_id}`);
+const pdf_url = getDocumentUrl(props.doc_id);
+const img_url = getImageUrl(props.image_id);
 
 const visible = ref(false);
 
@@ -68,6 +78,15 @@ const handleCancel = () => {
 </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;

+ 2 - 1
src/components/refference-effect/RefferenceRoot.vue

@@ -1,5 +1,5 @@
 <template>
-    <RefferenceFlag :img="img" :text="text" :doc_id="doc_id" :doc_name="doc_name" :positions="positions"/>
+    <RefferenceFlag :img="img" :text="text" :doc_id="doc_id" :doc_name="doc_name" :positions="positions" :image_id="image_id"/>
 </template>
 
 <script setup>
@@ -7,6 +7,7 @@ import RefferenceFlag from './RefferenceFlag.vue'
 
 const props = defineProps({
     img: String, // 图片链接
+    image_id: String,
     text: String, // 文本地址
     doc_id: String,
     doc_name: String,

+ 4 - 0
src/components/refference-effect/format-anwser-string.js

@@ -11,6 +11,10 @@ export const REFFERENCE_FLAG = 'refference-flag';
  * @returns 
  */
 export default function formatAnwserString(string) {
+    if (!string) {
+        return '';
+    }
+
     const result = marked.parse(string);
 
     const reg = /<span class="circle" data-list-index="\d"><\/span>/g;

+ 1 - 0
src/components/refference-effect/refference-flag.js

@@ -28,6 +28,7 @@ export default function dynamicRegRefFlag(mount_element, propsObject = {}) {
      */
     const app = createApp(RefferenceRoot, {
         img: propsObject.refference.img_id || '',
+        image_id: propsObject.refference.img_id,
         text: propsObject.refference.content_with_weight || '',
         doc_id: propsObject.refference.doc_id,
         doc_name: propsObject.refference.docnm_kwd,

+ 1 - 1
src/config/settings.json

@@ -14,5 +14,5 @@
   "tabBar": false,
   "menuFromServer": false,
   "serverMenu": [],
-  "RAGFlowUrl": ""
+  "RAGFlowUrl": "http://192.168.20.119:11080"
 }

+ 23 - 2
src/views/Home.vue

@@ -1,6 +1,7 @@
 <template>
   <div id="app">
     <Slider @select-nav="selectNav" :navId="navId" v-show="!closeOthers"></Slider>
+
     <div class="main">
       <div :class="{ 'history-wrap': true, 'history-wrap-open': !closeOthers && navId > -1 }">
         <History :list="historyList" @set-new="() => {
@@ -10,8 +11,10 @@
         }
           " :selectId="selectId" @handle-click="selectHistory" />
       </div>
+
       <div class="wrap">
         <Index :number="swiperNumber" v-if="navId === -1" @select-nav="selectNav" />
+        
         <div :class="{ 'border-wrap': true, 'border-no-wrap': navId === -2 }" v-else>
           <div>
             <header v-if="!showDialog">
@@ -23,6 +26,7 @@
                 <span>{{ slider.find(item => item.id === navId).dialog }}</span>
               </div>
             </header>
+
             <section :class="{ 'section-padding': showDialog }">
               <!-- 报告生成 -->
               <DocumentCombine v-if="navId == 0 && !showDialog" />
@@ -45,11 +49,13 @@
 
               <!-- 文库详情 -->
               <DocumentDetail v-if="navId === -2" @go-back="goBack" />
+
               <dialog-wrap v-if="showDialog && initDialog" v-show="!hideDialog" :type="dialogType[navId]"
                 :isSetting="false" :readonly="readonly" @setReadonly="setReadonly" @changeParse="changeParse"
                 @goDocument="goDocument" :agentId="selectId" :dialogId="dialogId" :list="dialogList" />
             </section>
-            <footer v-if="navId !== -2 && navId !== 0 && navId !== 3 && !historyMode">
+
+            <footer v-if="navId !== -2 && navId !== 0 && navId !== 3 && navId !== 6 && !historyMode">
               <message-input :readonly="readonly" eventName="open-dialog" />
             </footer>
 
@@ -60,6 +66,7 @@
                     <span>{{ item.variable_name }}</span>
                   </p>
                 </div>
+
                 <div>
                   <a-input :style="{ flex: '1' }" :placeholder="`请输入${item.variable_name}`" allow-clear @change="(event) => {
                     customChange(event, item.node_id, item.variable_name);
@@ -71,6 +78,7 @@
                     " :show-upload-button="{ showOnExceedLimit: true }" accept=".pdf,.doc,.docx,.txt,.md,.ppt.pptx" />
                 </div>
               </section>
+
               <a-textarea :max-length="maxLength" v-model="textValue"
                 :show-word-limit="textValue.length > maxLength / 2" :auto-size="{ maxRows: 6 }"
                 placeholder="输入您想了解的内容,Shift+Enter换行" :style="{
@@ -84,8 +92,10 @@
                   paddingBottom: `${textValue.length > maxLength / 2 ? '20px' : ''}`
                 }">
               </a-textarea>
+              
               <a-button :style="{ width: '500px' }" type="primary" @click="beginCombine">开始</a-button>
             </footer>
+
             <footer v-if="navId === 3 && !showDialog" class="upload-footer">
               <section v-for="(item, key) in variables" :key="key">
                 <div>
@@ -123,6 +133,11 @@
               </a-textarea>
               <a-button :style="{ width: '500px' }" type="primary" @click="continueCombine">开始</a-button>
             </footer>
+
+            <footer v-if="navId === 6">
+              <MessagePicUpload />
+            </footer>
+
             <a-tooltip :content="closeOthers ? '收起' : '展开'">
               <div @click="openAll" v-if="navId !== -2">
                 <img src="@/assets/other/open.svg" width="20" v-if="closeOthers" />
@@ -142,6 +157,7 @@ import DialogWrap from "../components/DialogWrap.vue";
 import Slider from "../components/Slider.vue";
 import History from "../components/History.vue";
 import MessageInput from "../components/MessageInput.vue";
+import MessagePicUpload from '../components/MessagePicUpload.vue'
 import DocumentDetail from "./DocumentDetail.vue";
 import DocumentCombine from "./DocumentCombine.vue";
 import Index from "./Index.vue";
@@ -164,6 +180,7 @@ export default defineComponent({
     Index,
     Answer,
     MessageInput,
+    MessagePicUpload,
     DialogWrap,
     Document,
     PicArticle,
@@ -298,10 +315,14 @@ export default defineComponent({
           });
         }
       }
+      
       // 如果不是历史记录对话模式,则创建对话
       if (!historyMode.value) {
         const data = await ajax.message.createDialog(selectId.value);
-        dialogId.value = data.chat_id;
+
+        if(data){
+          dialogId.value = data.chat_id;
+        }
       }
       dialogList.value = [];
       showDialog.value = true;

+ 3 - 3
src/views/PicArticle.vue

@@ -1,12 +1,12 @@
 <template>
-    <document-cards :data="data" :hasUpload="true" :hasSlider="false" :uploadImg="true" />
+    <picture-cards :data="data" :hasUpload="true" :hasSlider="false" :uploadImg="true" />
 </template>
 <script>
-import DocumentCards from "@/components/DocumentCards.vue";
+import PictureCards from '@/components/PictureCards.vue'
 import { defineComponent, ref, inject, reactive } from "vue";
 export default defineComponent({
     components: {
-        DocumentCards
+        PictureCards
     },
     setup() {
         const data = ref([