Bläddra i källkod

feat: 新增大模型回答时的图片展示组件

1. 新增大模型回答图片展示组件
2. 添加几个小图标
3. 修复小数绘图底部上传组件
zhupei 1 år sedan
förälder
incheckning
7b0c9e5974

+ 10 - 5
README.md

@@ -10,18 +10,23 @@ 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`命令可能无法启动项目。
+
 ## 相关资源
 
-* [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/)

+ 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>

+ 6 - 1
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">
@@ -93,9 +94,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" />
@@ -111,6 +114,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">
@@ -119,6 +123,7 @@
               <!-- 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">
@@ -149,8 +154,8 @@
               </div>
             </section>
           </div>
-          <!-- 如果有图片 -->
 
+          <!-- 如果有图片 -->
           <div v-if="item?.image_url">
             <a-image width="500" :src="item?.image_url" />
           </div>

+ 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;

+ 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;

+ 16 - 1
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,6 +11,7 @@
         }
           " :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>
@@ -23,6 +25,7 @@
                 <span>{{ slider.find(item => item.id === navId).dialog }}</span>
               </div>
             </header>
+
             <section :class="{ 'section-padding': showDialog }">
               <!-- 报告生成 -->
               <DocumentCombine v-if="navId == 0 && !showDialog" />
@@ -43,11 +46,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>
 
@@ -69,6 +74,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="{
@@ -82,8 +88,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>
@@ -121,6 +129,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" />
@@ -140,6 +153,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";
@@ -161,6 +175,7 @@ export default defineComponent({
     Index,
     Answer,
     MessageInput,
+    MessagePicUpload,
     DialogWrap,
     Document,
     PicArticle,

+ 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([