wujingjing
2024-12-31 1d0eec6da86e3e9bcbf002d1a22142586fe023a3
src/components/chat/Chat.vue
@@ -1,200 +1,153 @@
<template>
   <div class="flex h-full">
      <div class="flex flex-col h-full flex-auto">
         <div class="h-full flex flex-col items-center overflow-y-auto">
            <div ref="chatListDom" class="h-full" :style="{ width: chatWidth }">
               <div
                  class="group flex px-4 py-6 hover:bg-slate-100 rounded-lg relative"
                  :class="{ 'flex-row-reverse': item.role === RoleEnum.user }"
                  v-for="(item, index) of computedMessageList"
                  :key="index"
               >
                  <img
                     class="rounded-full size-12 flex-0"
                     :class="{ 'mr-4': item.role === RoleEnum.assistant, 'ml-4': item.role === RoleEnum.user }"
                     :src="roleImageMap[item.role]"
                     alt=""
                     srcset=""
                  />
                  <div class="flex-auto flex" :class="{ 'justify-end': item.role === RoleEnum.user }">
                     <div class="inline-flex flex-col" :class="{ 'w-full': item.role === RoleEnum.assistant }">
                        <div class="w-full" v-if="item.content?.values">
                           <div
                              class="text-sm rounded-[6px] p-4 leading-relaxed"
                              :style="{ backgroundColor: item.role === RoleEnum.user ? 'rgb(197 224 255)' : 'white' }"
                           >
                              <div v-if="item.content.errCode === ErrorCode.Message" class="flex-column w-full">
                                 <p class="text-red-500">
                                    {{ item.content.errMsg }}
                                 </p>
                                 <div class="mt-5 flex items-center" v-if="showFixQuestion(item)">
                                    <div class="text-gray-600 flex-0">
                                       {{ item.content.origin.err_json.fix_question.title + ':' }}
                                    </div>
                                    <div class="ml-1 space-x-2 inline-flex flex-wrap">
                                       <div
                                          v-for="fixItem in item.content.origin.err_json.fix_question?.values"
                                          :key="fixItem"
                                          class="bg-gray-200 p-3 hover:bg-[#c5e0ff] hover:text-[#1c86ff] cursor-pointer rounded-lg"
                                          @click="fixQuestionClick(fixItem, item.content.origin)"
                                       >
                                          {{ fixItem.title }}
                                       </div>
                                    </div>
                                 </div>
                              </div>
                              <template v-else>
                                 <component :is="answerTypeMapCom[item.content.type]" :data="item.content.values" :originData="item" />
   <ChatContainer
      :loading="chatListLoading"
      :more-is-loading="moreIsLoading"
      :is-share-page="isSharePage"
      :chat-width="chatWidth"
      ref="containerRef"
   >
      <!-- 消息列表 -->
      <template #message-list>
         <template v-if="computedMessageList?.length > 0">
            <div v-for="(item, msgIndex) of computedMessageList" :key="`${item.historyId}_${item.role}`">
               <UserMsg
                  :msg="item"
                  @shareClick="shareClick"
                  @setCommonQuestion="setCommonQuestionClick"
                  v-if="item.role === RoleEnum.user"
               ></UserMsg>
                                 <div
                                    v-if="item.role === RoleEnum.assistant && item.content.origin?.ext_call_list"
                                    class="flex font-bold items-center mt-6"
                                 >
                                    <div class="flex-0 mb-auto -mr-4">关联功能:</div>
                                    <div class="space-x-5 flex flex-wrap">
                                       <div
                                          v-for="callItem in item.content.origin?.ext_call_list"
                                          :key="callItem.call_ext_id"
                                          @click="relativeQueryClick(callItem)"
                                          class="cursor-pointer hover:underline first-of-type:ml-5"
                                       >
                                          {{ callItem.question }}
                                       </div>
                                    </div>
                                 </div>
                              </template>
                           </div>
                           <!-- 操作 -->
                           <div v-if="item.role === RoleEnum.assistant" class="absolute flex items-center right-0 mr-4 mt-2 space-x-2">
                              <div
                                 class="flex items-center justify-center size-[15px]"
                                 v-if="item.content?.type === AnswerType.Text || item.content?.type === AnswerType.Knowledge"
                              >
                                 <i
                                    class="p-2 ywifont ywicon-copy cursor-pointer hover:text-[#0284ff] hover:!text-[18px]"
                                    @click="copyClick(item)"
                                 />
                              </div>
                              <template v-if="item.content.errCode !== ErrorCode.Message">
                                 <div class="flex items-center justify-center size-[15px]">
                                    <i
                                       :class="{ 'text-[#0284ff]': item.state === AnswerState.Like }"
                                       class="p-2 ywifont ywicon-dianzan cursor-pointer hover:text-[#0284ff] font-medium hover:!text-[18px]"
                                       @click="likeClick(item)"
                                    />
                                 </div>
                                 <div class="flex items-center justify-center size-[15px]">
                                    <i
                                       :class="{ 'text-[#0284ff]': item.state === AnswerState.Unlike }"
                                       class="p-2 ywifont ywicon-buzan cursor-pointer hover:text-[#0284ff] !text-[13px] hover:!text-[15px]"
                                       @click="unLikeClick(item)"
                                    />
                                 </div>
                              </template>
                              <div class="flex items-center justify-center size-[15px] relative">
                                 <i
                                    class="p-2 ywifont ywicon-wentifankui cursor-pointer hover:text-[#0284ff] !text-[13px] hover:!text-[15px]"
                                    @click="
                                       ($event) =>
                                          feedbackClick(
                                             $event,
                                             item,
                                             computedMessageList
                                                .filter((v) => v.role === RoleEnum.assistant)
                                                .findIndex((v) => v.historyId === item.historyId)
                                          )
                                    "
                                 />
                                 <FeedbackPanel
                                    v-show="feedbackIsShow && currentFeedbackMapItem === item"
                                    ref="feedbackPanelRef"
                                    v-model:isShow="feedbackIsShow"
                                    v-model:content="feedbackContent"
                                    :chatItem="currentFeedbackMapItem"
                                    :position="feedbackPosition"
                                 />
                              </div>
                           </div>
                        </div>
                        <Loding v-if="isTalking && index === computedMessageList.length - 1" class="w-fit" :process="process" />
                     </div>
                  </div>
               </div>
               <div v-if="showAskMore" class="ml-4 mt-5 text-sm">
                  <div class="text-gray-600 mb-5">你可以继续问我:</div>
                  <div class="space-y-2 inline-flex flex-col">
                     <div
                        v-for="item in computedMessageList.at(-1).content.askMoreList"
                        :key="item.history_id"
                        class="bg-white p-3 hover:bg-[#c5e0ff] hover:text-[#1c86ff] cursor-pointer rounded-lg"
                        @click="askMoreClick(item)"
                     >
                        {{ item.question }}
                     </div>
               <AssistantMsg
                  v-else
                  :msg="item"
                  :msgList="computedMessageList"
                  :isLast="msgIndex === computedMessageList.length - 1"
                  @sendChatMessage="sendChatMessage"
                  @shareMsg="shareClick"
                  :isTalking="isTalking"
               />
            </div>
            <div v-if="showAskMore" class="ml-4 mt-5 pb-10">
               <div class="text-gray-600 mb-5">你可以继续问我:</div>
               <div class="space-y-2 inline-flex flex-col">
                  <div
                     v-for="item in computedMessageList.at(-1).content.askMoreList"
                     :key="item.history_id"
                     class="bg-white p-3 hover:bg-[#c5e0ff] hover:text-[#1c86ff] cursor-pointer rounded-lg"
                     @click="askMoreClick(item)"
                  >
                     {{ item.question }}
                  </div>
               </div>
            </div>
         </div>
         </template>
         <el-empty v-else-if="isSharePage && !chatListLoading" :image-size="200">
            <template #description>
               <span class="text-[15px]">分享的对话不存在或已失效</span>
            </template>
         </el-empty>
      </template>
         <div class="sticky bottom-0 w-full p-6 pb-8 bg-[rgb(247,248,250)] flex justify-center">
            <PlayBar
               v-model:voicePageIsShow="voicePageIsShow"
               :isTalking="isTalking"
               :isHome="false"
               v-model="messageContent.values"
               @sendClick="sendClick"
               :style="{ width: chatWidth }"
            ></PlayBar>
         </div>
      </div>
      <!-- 输入区域 -->
      <template #input-area>
         <PlayBar
            v-model:voicePageIsShow="voicePageIsShow"
            :isTalking="isTalking"
            :isHome="false"
            v-model="messageContent.values"
            @sendClick="sendClick"
            @showUpChatClick="showUpChatClick"
            @stopGenClick="stopGenClick"
            @showDownChatClick="showDownChatClick"
            :style="{ width: chatWidth }"
            :setCommonQuestionInfo="setCommonQuestionInfo"
         />
      </template>
      <CustomDrawer v-model:isShow="drawerIsShow" @updateChatInput="updateChatInput" />
   </div>
      <!-- 抽屉 -->
      <template #drawer>
         <CustomDrawer v-model:isShow="drawerIsShow" @updateChatInput="updateChatInput" />
         <ShareLinkDlg ref="shareLinkDlgRef" />
      </template>
   </ChatContainer>
</template>
<script setup lang="ts">
import { ElMessage } from 'element-plus';
import _ from 'lodash';
import { v4 as uuidv4 } from 'uuid';
import { computed, onMounted, ref } from 'vue';
import FeedbackPanel from './components/FeedbackPanel.vue';
import Loding from './components/Loding.vue';
import { useAssistantContentOpt } from './hooks/useAssistantContentOpt';
import { useQueryProcess } from './hooks/useQueryProcess';
import { useScrollToBottom } from './hooks/useScrollToBottom';
import type { CancelTokenSource } from 'axios';
import axios from 'axios';
import { orderBy } from 'lodash-es';
import moment from 'moment';
import { computed, onActivated, onMounted, ref } from 'vue';
import { loadAmisSource } from '../amis/load';
import { convertProcessItem, convertProcessToStep, formatShowTimeYear, useScrollLoad } from './hooks/useScrollLoad';
import type { ChatContent } from './model/types';
import { AnswerState, AnswerType, RoleEnum, answerTypeMapCom, roleImageMap, type ChatMessage } from './model/types';
import { GetHistoryAnswer, QueryHistoryDetail, QuestionAi, extCallQuery } from '/@/api/ai/chat';
import { AnswerState, AnswerType, RoleEnum, type ChatMessage } from './model/types';
import { getShareChatJsonByPost, questionStreamByPost } from '/@/api/ai/chat';
import PlayBar from '/@/components/chat/components/playBar/PlayBar.vue';
import CustomDrawer from '/@/components/drawer/CustomDrawer.vue';
import router from '/@/router';
import { activeChatRoom, activeLLMId, activeSampleId, activeSectionAId, getRoomConfig, roomConfig } from '/@/stores/chatRoom';
import { ErrorCode } from '/@/utils/request';
import { Logger } from '/@/model/logger/Logger';
import { ElMessage } from 'element-plus';
import AssistantMsg from './assistant/index.vue';
import ChatContainer from './components/ChatContainer.vue';
import ShareLinkDlg from './components/shareLink/index.vue';
import UserMsg from './user/index.vue';
import router from '/@/router';
import {
   activeChatRoom,
   activeGroupType,
   activeLLMId,
   activeRoomId,
   activeSampleId,
   isSharePage,
   roomConfig,
} from '/@/stores/chatRoom';
import emitter from '/@/utils/mitt';
import { useCompRef } from '/@/utils/types';
import { toMyFixed } from '/@/utils/util';
const containerRef = useCompRef(ChatContainer);
const chatListDom = computed(() => containerRef.value?.chatListDom);
const chatWidth = '75%';
const voicePageIsShow = ref(false);
let isTalking = ref(false);
let messageContent = ref<ChatContent>({
   type: AnswerType.Text,
   values: '',
});
const currentRoute = router.currentRoute;
const currentRouteId = currentRoute.value.query.id as string;
const chatListDom = ref<HTMLDivElement>();
activeRoomId.value = currentRouteId;
const messageList = ref<ChatMessage[]>([]);
const computedMessageList = computed(() => {
   return messageList.value.filter((v) => !!v);
});
const parseContent = (res) => {
const parseExtraContent = (res) => {
   if (!res) return {};
   const askMoreList = orderBy(res.context_history, [(item) => Number(item.radio)], ['desc']);
   const errCode = res?.err_code;
   const errMsg = res?.json_msg;
   const origin = res;
   return {
      askMoreList,
      errCode,
      errMsg,
      origin,
   };
};
const parseContent = (res, reportIsShow = false, extraContent?) => {
   if (!res) return null;
   let content: ChatContent = {
      type: AnswerType.Text,
      values: '解析失败!',
   };
   if (res.type) {
      res.answer_type = res.type;
   }
   const curExtraContent = parseExtraContent(res);
   switch (res.answer_type) {
      case AnswerType.RecordSet:
@@ -209,6 +162,12 @@
            values: res.values ?? res.answer,
         };
         break;
      case AnswerType.Script:
         content = {
            type: AnswerType.Script,
            values: res,
         };
         break;
      case AnswerType.Knowledge:
         content = {
@@ -217,10 +176,22 @@
         };
         break;
      case AnswerType.Report:
         content = {
            type: AnswerType.Report,
            values: (res?.reports ?? []).map((item) => ({
               content: parseContent(item, reportIsShow, { origin: item, conclusion: item.conclusion ?? [] }),
            })),
         };
         break;
      case AnswerType.Summary:
         content = {
            type: AnswerType.Summary,
            values: res.summary,
            values: res.summary?.map((item) => {
               item.reportIsShow = reportIsShow;
               return item;
            }),
         };
         break;
      case AnswerType.Url:
@@ -242,75 +213,230 @@
         };
         break;
   }
   content.askMoreList = _.orderBy(res.context_history, [(item) => Number(item.radio)], ['desc']);
   content.errCode = res?.err_code;
   content.errMsg = res?.json_msg;
   content.origin = res;
   if (!extraContent) {
      content = {
         ...content,
         ...curExtraContent,
      };
   } else {
      content = {
         ...content,
         ...extraContent,
      };
   }
   return content;
};
const { clearQueryProcess, process, processId, queryProcess } = useQueryProcess();
const DEFAULT_SECTION_A_ID = 'knowledge_base';
let questionRes = null;
let position = null;
const preQuestion = ref(null);
let finalCalcSectionAId = null;
let lastAxiosSource: CancelTokenSource = null;
const questionAi = async (text) => {
   processId.value = uuidv4();
   let judgeParams = null;
   if (!preQuestion.value) {
      // const aiContent = computedMessageList.value.filter((item) => item.role === RoleEnum.assistant);
      // const lastQuestion = aiContent[aiContent.length - 2]?.content?.origin?.question;
      // judgeParams = lastQuestion
      //    ? {
      //          prev_question: lastQuestion,
      //      }
      //    : {};
      // 正常回答暂时不采用
      judgeParams = {};
   } else {
      judgeParams = {
         prev_question: preQuestion.value,
      };
   }
   let currentSectionAId = '';
   if (activeSectionAId.value) {
      currentSectionAId = activeSectionAId.value;
      activeSectionAId.value = '';
   } else {
      const lastSectionAItem = _.findLast(
         computedMessageList.value as any,
         (item) => item.role === RoleEnum.assistant && !!item.sectionAId
      );
      currentSectionAId = lastSectionAItem?.sectionAId ?? DEFAULT_SECTION_A_ID;
   }
   finalCalcSectionAId = currentSectionAId;
   const params = {
      process_id: processId.value,
      question: text,
      // FIXME: 暂时这样
      section_a_id: currentSectionAId,
      history_group_id: currentRouteId,
      raw_mode: roomConfig.value?.[currentRouteId]?.isAnswerByLLM ?? false,
      ...judgeParams,
   } as any;
   if (position) {
      const longitude = position.coords.longitude;
      const latitude = position.coords.latitude;
      params.cur_pos = [longitude, latitude].join(',');
   }
   if (activeGroupType.value) {
      params.group_type = activeGroupType.value;
   }
   if (currentSampleId) {
      params.sample_id = currentSampleId;
      currentSampleId = '';
   }
   // if (currentLLMId) {
   //    params.llm_id = currentLLMId;
   // }
   clearQueryProcess();
   queryProcess();
   const res = await QuestionAi(params).finally(() => {
      clearQueryProcess();
   let lastTimestamp = new Date().getTime();
   questionRes = {};
   let lastIsResult = false;
   const resultP = new Promise((resolve, reject) => {
      const currentSource = axios.CancelToken.source();
      lastAxiosSource = currentSource;
      const getResReport = () => {
         const resReport = {
            answer_type: AnswerType.Report,
            reports: [],
         };
         return resReport;
      };
      const checkReportEmpty = () => {
         const isEmpty = !questionRes?.reports || questionRes?.reports?.length === 0;
         return isEmpty;
      };
      questionStreamByPost(
         params,
         (chunkRes) => {
            Logger.info('chunk response:\n\n' + JSON.stringify(chunkRes));
            if (chunkRes.mode === 'result') {
               lastIsResult = true;
               const res = chunkRes.value;
               if (checkReportEmpty()) {
                  const resReport = getResReport();
                  resReport.reports.push(res);
                  questionRes = resReport;
                  resolve(resReport);
               } else {
                  const lastMsg = computedMessageList.value.at(-1);
                  // 已经解析过一次 reports
                  lastMsg.content.values.push({
                     content: parseContent(res, true, {
                        origin: res,
                     }),
                  });
               }
               return;
               // chunkRes.value = '准备数据分析';
            }
            if (chunkRes.mode === 'summary') {
               const lastMsg = computedMessageList.value.at(-1);
               const extraContent = parseExtraContent(chunkRes.value);
               const isReportEmpty = checkReportEmpty();
               // 没有经过 result 报告还没初始化
               if (isReportEmpty) {
                  const resReport = getResReport();
                  questionRes = resReport;
               }
               // 此对话已经加入到对话列表
               if (lastMsg.content?.values && extraContent) {
                  for (const key in extraContent) {
                     if (Object.prototype.hasOwnProperty.call(extraContent, key)) {
                        const value = extraContent[key];
                        if (!lastMsg.content[key] || (Array.isArray(lastMsg.content[key]) && lastMsg.content[key].length === 0)) {
                           lastMsg.content[key] = value;
                        }
                     }
                  }
                  lastMsg.historyId = chunkRes.value.history_id;
                  const userMsg = computedMessageList.value.at(-2);
                  userMsg.historyId = chunkRes.value.history_id;
                  userMsg.content.values = chunkRes.value.question;
               }
               if (Object.keys(questionRes).length === 0) {
                  questionRes = chunkRes.value;
               }
               // 此对话还未加入到对话列表
               if (!lastMsg.content?.values && questionRes) {
                  questionRes = {
                     ...questionRes,
                     ...chunkRes.value,
                  };
               }
               if (isReportEmpty) {
                  resolve(questionRes);
               }
               // computedMessageList.value[computedMessageList.value.length - 1] = finalMsg;
               scrollToBottom();
               // chunkRes.value = '你可以继续问我';
               return;
            }
            if (chunkRes.mode === 'conclusion') {
               const lastReport = computedMessageList.value.at(-1)?.content?.values?.at(-1);
               if (lastReport) {
                  lastReport.conclusion = chunkRes.value;
                  chunkRes.value = '分析结束';
               }
            }
            if (chunkRes.mode === 'question') {
               const lastGroup = computedMessageList.value.at(-1).stepGroup.at(-1);
               const stepList = lastGroup?.value ?? [];
               const lastStepItem = stepList.at(-1);
               if (!lastStepItem.subStep) {
                  lastStepItem.subStep = [];
               }
               lastStepItem.subStep.push({
                  type: chunkRes.value.type,
                  data: chunkRes.value,
               });
               scrollToBottom();
               return;
            }
            // 暂时不考虑多个 report情况
            // if (lastIsResult && chunkRes.mode !== 'finish') {
            //    // 开始增加新的 stepGroup
            //    computedMessageList.value.at(-1).stepGroup.push({
            //       value: [],
            //       isShow: true,
            //    });
            //    lastIsResult = false;
            // }
            const lastGroup = computedMessageList.value.at(-1).stepGroup.at(-1);
            const stepList = lastGroup?.value ?? [];
            const currentTimeStamp = new Date().getTime();
            const ms = toMyFixed(currentTimeStamp - lastTimestamp, 2) + ' ms';
            if (chunkRes.mode === 'finish') {
               stepList.at(-1).ms = ms;
               isTalking.value = false;
               return;
            }
            if (stepList?.length >= 1) {
               stepList.at(-1).ms = ms;
            } else {
               const stepGroup = computedMessageList.value.at(-1).stepGroup;
               if (stepGroup.length > 1) {
                  const lastStepList = stepGroup.at(-2).value;
                  lastStepList.at(-1).ms = ms;
               }
            }
            lastTimestamp = currentTimeStamp;
            const stepItem = convertProcessItem(chunkRes);
            stepList.push(stepItem);
            // 强制触发更新
            scrollToBottom();
         },
         {
            cancelToken: currentSource.token,
         }
      )
         .catch((err) => {
            throw err;
         })
         .finally(() => {
            isTalking.value = false;
            // 收起所有 stepGroup
            computedMessageList.value.at(-1).stepGroup.forEach((item) => {
               item.isShow = false;
            });
         });
   });
   questionRes = res;
   const content = parseContent(res);
   await resultP;
   const content = parseContent(questionRes, true);
   return content;
};
@@ -324,14 +450,55 @@
let currentLLMId = null;
const getAnswerById = async (historyId: string) => {
   return await GetHistoryAnswer({
      history_id: historyId,
   });
const stopGenClick = () => {
   lastAxiosSource?.cancel();
   isTalking.value = false;
   chatListLoading.value = false;
   computedMessageList.value.at(-1).isStopMsg = true;
};
const sendChatMessage = async (content: ChatContent = messageContent.value, cb?: any, isCallExtParams?: any) => {
   if (!content?.values || isTalking.value) return;
const checkCanSend = (content: ChatContent = messageContent.value) => {
   if (!content?.values) {
      return false;
   }
   if (isTalking.value || chatListLoading.value) {
      ElMessage.warning('ai 正在回复中,请稍后尝试提问');
      return false;
   }
   return true;
};
const addChatItem = (content: ChatContent) => {
   isTalking.value = true;
   const userItem: ChatMessage = { role: RoleEnum.user, content, isChecked: false } as any;
   const assistantItem: ChatMessage = {
      role: RoleEnum.assistant,
      content: {
         type: AnswerType.Report,
      },
      state: AnswerState.Null,
      stepGroup: [
         {
            value: [],
            isShow: true,
         },
      ],
      isStopMsg: false,
      isChecked: false,
   } as any;
   messageList.value.push(userItem);
   clearMessageContent();
   messageList.value.push(assistantItem);
   scrollToBottom();
   return [userItem, assistantItem];
};
const sendChatMessage = async (content: ChatContent = messageContent.value) => {
   if (!checkCanSend(content)) {
      return;
   }
   const isNewChat = messageList.value.length === 0;
   if (isNewChat) {
      if (activeSampleId.value) {
@@ -345,160 +512,171 @@
   let resMsgContent: ChatContent = null;
   try {
      isTalking.value = true;
      const userItem: ChatMessage = { role: RoleEnum.user, content } as any;
      const assistantItem: ChatMessage = { role: RoleEnum.assistant, content: null, state: AnswerState.Null } as any;
      // 发送当前
      messageList.value.push(userItem);
      // 清空输入框
      clearMessageContent();
      const [userItem, assistantItem] = addChatItem(content);
      // 出现回复,置空出现等待动画
      messageList.value.push(assistantItem);
      if (isCallExtParams) {
         const extRes = await extCallQuery(isCallExtParams);
         questionRes = extRes;
         resMsgContent = parseContent(extRes);
      } else {
         resMsgContent = await questionAi(content.values);
      }
      resMsgContent = await questionAi(content.values);
      if (isNewChat) {
         const firstResCb = getRoomConfig(currentRouteId, 'firstResCb');
         firstResCb?.(resMsgContent);
      } else {
         cb?.(resMsgContent);
      }
      userItem.historyId = questionRes.history_id;
      updateLoadIndex();
      userItem.historyId = questionRes?.history_id;
      userItem.content.values = questionRes?.question ?? userItem.content.values;
      assistantItem.historyId = questionRes.history_id;
      assistantItem.sectionAId = finalCalcSectionAId;
      appendLastMessageContent(resMsgContent);
   } catch (error: any) {
      // appendLastMessageContent({
      //    type: AnswerType.Text,
      //    values: '发生错误!',
      // });
   } finally {
      isTalking.value = false;
      assistantItem.historyId = questionRes?.history_id;
      const currentTime = formatShowTimeYear(moment().format('YYYY-MM-DD HH:mm:ss'));
      assistantItem.createTime = currentTime;
      assistantItem.content = resMsgContent;
      setTimeout(() => {
         // 收到回复,继续滚
         scrollToBottom();
      }, 300);
   } catch (error: any) {}
};
const sendClick = () => {
   sendChatMessage(messageContent.value);
};
const { loadRangeData, onChatListScroll, moreIsLoading, updateLoadIndex } = useScrollLoad({
   container: chatListDom,
   historyGroupId: currentRouteId,
   messageList,
   parseAnswerContent: parseContent,
});
const chatListLoading = ref(true);
onActivated(() => {
   emitter.emit('updateHeaderTitle', activeChatRoom.value?.title ?? '');
});
const initNewChat = () => {
   messageContent.value = {
      type: AnswerType.Text,
      values: activeChatRoom.value?.title,
   };
   sendChatMessage();
};
const scrollToBottom = () => {
   containerRef.value?.scrollToBottom();
};
const initHistoryChat = () => {
   // 初始状态滚一下
   scrollToBottom();
   setTimeout(() => {
      chatListDom.value.addEventListener('scroll', onChatListScroll);
   }, 300);
};
/**
 * 加载分享数据
 */
const loadShareData = async () => {
   const res = await getShareChatJsonByPost({
      share_id: router.currentRoute.value.query.id as string,
   });
   const msgValue = res?.values;
   if (!msgValue) {
      messageList.value = [];
      return;
   }
};
   const userMsg: ChatMessage = {
      historyId: msgValue.history_id,
      role: RoleEnum.user,
      content: {
         type: AnswerType.Text,
         values: msgValue.question,
      },
      isChecked: false,
   };
const sendClick = (cb) => {
   sendChatMessage(messageContent.value, cb);
};
const appendLastMessageContent = (content: ChatContent) => {
   if (messageList.value.at(-1)) {
      messageList.value.at(-1).content = content;
   }
};
   const assistantMsg: ChatMessage = {
      historyId: msgValue.history_id,
      role: RoleEnum.assistant,
      content: parseContent(msgValue),
      stepGroup: (msgValue?.reports ?? []).map((item) => ({
         value: convertProcessToStep(item?.exec_process),
         isShow: false,
      })),
      isStopMsg: false,
// 一次性加载最近条数限制
const LOAD_CHAT_LIMIT = 10;
// 所有用户提问历史记录
let userMsgHistory = [];
// 下次加载用户提问索引位置
let nextUserMsgEndIndex = 0;
      conclusion: msgValue.conclusion ?? [],
      isChecked: false,
   };
   messageList.value = [userMsg, assistantMsg];
};
onMounted(async () => {
   const res = await QueryHistoryDetail({
      history_group_id: currentRouteId,
   });
   userMsgHistory = res.details ?? [];
   nextUserMsgEndIndex = userMsgHistory.length;
   // 截取倒数 LOAD_CHAT_LIMIT 条用户消息
   const currentUserMsgList = userMsgHistory.slice(nextUserMsgEndIndex - LOAD_CHAT_LIMIT, nextUserMsgEndIndex);
   messageList.value = currentUserMsgList.map((item) => {
      return {
         historyId: item.history_id,
         role: RoleEnum.user,
         content: {
            type: AnswerType.Text,
            values: item.question,
         },
      } as ChatMessage;
   });
   const sectionAIdMap = new Map();
   // 获取结果插入到用户提问之后
   currentUserMsgList.map((item) => {
      sectionAIdMap.set(item.history_id, item.section_a_id);
      getAnswerById(item.history_id).then((aiRobot) => {
         // 用户提问索引
         const userMsgIndex = messageList.value.findIndex((subItem) => subItem?.historyId === item.history_id);
         const userMsg = messageList.value[userMsgIndex];
         // values 取回答之后最终的 question
         userMsg.content.values = aiRobot.answer?.question ?? userMsg.content.values;
         messageList.value.splice(
            userMsgIndex + 1,
            0,
            aiRobot.answer === null
               ? null
               : {
                     historyId: aiRobot.answer?.history_id,
                     role: RoleEnum.assistant,
                     content: parseContent(aiRobot.answer),
                     state: aiRobot.answer_state,
                     sectionAId: sectionAIdMap.get(aiRobot.answer.history_id),
                 }
         );
   messageList.value = [];
   chatListLoading.value = true;
   if (isSharePage.value) {
      await loadShareData().finally(() => {
         chatListLoading.value = false;
      });
   });
   } else {
      await loadRangeData().finally(() => {
         chatListLoading.value = false;
      });
   }
   setTimeout(() => {
      emitter.emit('updateHeaderTitle', activeChatRoom.value?.title ?? '');
   }, 300);
   if (messageList.value.length === 0) {
      messageContent.value = {
         type: AnswerType.Text,
         values: activeChatRoom.value.title,
      };
      sendChatMessage();
   }
});
const { forbidScroll } = useScrollToBottom({
   chatListDom: chatListDom,
   displayMessageList: computedMessageList,
});
//#region ====================== 关联查询 ======================
const relativeQueryClick = async (val) => {
   sendChatMessage(
      {
         type: AnswerType.Text,
         values: val.question,
      },
      undefined,
      {
         history_group_id: currentRouteId,
         question: val.question,
         call_ext_id: val.call_ext_id,
         call_ext_args: val.agrs ? JSON.stringify(val.agrs) : null,
      initNewChat();
   } else {
      if (!isSharePage.value) {
         setTimeout(() => {
            initHistoryChat();
         }, 300);
      }
   );
   }
   loadAmisSource();
});
//#region ====================== 光标输入上下箭头显示历史消息 ======================
const currentIndex = ref(null);
const history_data = computed(() => {
   return computedMessageList.value.filter((item) => item.role === RoleEnum.user);
});
//显示上一条消息
const showUpChatClick = () => {
   if (computedMessageList.value.length === 0) return;
   if (currentIndex.value == 0) {
      messageContent.value.values = history_data.value[currentIndex.value].content.values;
      return;
   } else {
      currentIndex.value = (currentIndex.value + history_data.value.length - 1) % history_data.value.length;
   }
   messageContent.value.values = history_data.value[currentIndex.value].content.values;
};
//显示下一条消息
const showDownChatClick = () => {
   if (computedMessageList.value.length === 0) return;
   if (currentIndex.value == history_data.value.length - 1) {
      messageContent.value.values = history_data.value[currentIndex.value].content.values;
      return;
   }
   if (currentIndex.value === null) {
      currentIndex.value = 0;
   } else {
      currentIndex.value = (currentIndex.value + 1) % history_data.value.length;
   }
   messageContent.value.values = history_data.value[currentIndex.value].content.values;
};
//#endregion
const {
   copyClick,
   likeClick,
   unLikeClick,
   feedbackPosition,
   feedbackIsShow,
   feedbackContent,
   feedbackPanelRef,
   currentFeedbackMapItem,
   feedbackClick,
   askMoreClick,
   fixQuestionClick,
   preQuestion,
   showFixQuestion,
   showAskMore,
} = useAssistantContentOpt({
   forbidScroll,
   sendChatMessage,
   displayMessageList: computedMessageList,
const showAskMore = computed(() => {
   if (!computedMessageList.value || computedMessageList.value.length === 0) return false;
   const last = computedMessageList.value.at(-1);
   const isShow = last?.role === RoleEnum.assistant && last?.content?.values && last.content?.askMoreList?.length > 0;
   const result = isShow && !isSharePage.value;
   return result;
});
const askMoreClick = (item) => {
   if (!item.question) return;
   sendChatMessage({ type: AnswerType.Text, values: item.question });
};
//#region ====================== 侧边栏drawer ======================
const drawerIsShow = ref(false);
@@ -507,12 +685,57 @@
   messageContent.value.values = content;
};
//#endregion
//#region ====================== 用户询问的问题设置为常用语 ======================
const setCommonQuestionInfo = ref({});
//用户问题设置为常用语
const setCommonQuestionClick = (item) => {
   setCommonQuestionInfo.value = item;
};
//#endregion
//#region ====================== 分享 ======================
const shareLinkDlgRef = useCompRef(ShareLinkDlg);
const shareClick = async (item: ChatMessage) => {
   shareLinkDlgRef.value.openShare(item);
};
//#endregion
</script>
<style scoped>
<style scoped lang="scss">
pre {
   font-family: -apple-system, 'Noto Sans', 'Helvetica Neue', Helvetica, 'Nimbus Sans L', Arial, 'Liberation Sans', 'PingFang SC',
      'Hiragino Sans GB', 'Noto Sans CJK SC', 'Source Han Sans SC', 'Source Han Sans CN', 'Microsoft YaHei', 'Wenquanyi Micro Hei',
      'WenQuanYi Zen Hei', 'ST Heiti', SimHei, 'WenQuanYi Zen Hei Sharp', sans-serif;
}
.more-loading {
   :deep(.el-loading-spinner) {
      --loading-size: 35px;
      margin-top: 0;
      .circular {
         width: var(--loading-size);
         height: var(--loading-size);
      }
   }
}
:deep(.el-step__icon.is-text) {
   --radius-size: 24px;
   width: var(--radius-size);
   height: var((--radius-size));
}
:deep(.el-step__icon-inner) {
   font-size: 16px !important;
}
:deep(.el-step__description) {
   min-height: 20px;
}
:deep(.el-step:last-of-type .el-step__description) {
   // display: none;
}
</style>