wujingjing
2024-12-18 605467fb93b73eb3d019933f7499ec8ad1837420
src/components/chat/Chat.vue
@@ -1,129 +1,271 @@
<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" />
      <div class="flex flex-col h-full flex-auto relative">
         <div ref="chatListDom" class="relative h-full flex flex-col items-center overflow-y-auto" style="height: calc(100% - 50px)">
            <span
               class="more-loading absolute text-blue-400 left-[50%] translate-x-[-50%] cursor-pointer w-10"
               v-loading="moreIsLoading"
            ></span>
            <div class="h-full relative" v-loading="chatListLoading" :style="{ width: chatWidth }">
               <template v-if="computedMessageList?.length > 0">
                  <div v-for="(item, msgIndex) of computedMessageList" :key="`${item.historyId}_${item.role}`">
                     <UserMsg
                        :msg="item"
                        @copyMsg="copyClick"
                        @shareClick="shareClick"
                        @setCommonQuestion="setCommonQuestionClick"
                        v-if="item.role === RoleEnum.user"
                     ></UserMsg>
                     <div v-else class="flex px-4 py-6 rounded-lg relative" :class="{ 'px-10': isShareCheck }">
                        <div class="absolute top-0 left-[72px] text-[#8d8e99]">{{ item?.createTime }}</div>
                        <!-- :class="{ 'top-[30px]': item.role === RoleEnum.user, 'top-[30px]': item.role === RoleEnum.assistant }" -->
                        <img
                           class="rounded-full size-12 flex-0"
                           :class="{ 'mr-4': item.role === RoleEnum.assistant }"
                           :src="roleImageMap[item.role]"
                           alt=""
                           srcset=""
                        />
                        <div class="flex-auto flex">
                           <div class="inline-flex flex-col" :class="{ 'w-full': item.role === RoleEnum.assistant }">
                              <div class="w-full">
                                 <div class="rounded-[6px] p-4 leading-relaxed bg-white">
                                    <!-- #region ====================== 意图分析 ======================-->
                                    <div class="flex flex-col" v-if="item?.stepList?.length > 0">
                                       <!-- #region ====================== 意图分析 ======================-->
                                       <div class="flex items-center">
                                          <span class="mr-2">意图分析:</span>
                                          <div
                                             @click="toggleStepList(item)"
                                             class="cursor-pointer border border-gray-300 border-solid w-fit px-2 flex items-center space-x-2 rounded-lg hover:bg-gray-100 active:bg-gray-200"
                                          >
                                             <span>
                                                {{ toggleStepLabel(item) }}
                                             </span>
                                             <span
                                                class="ywifont"
                                                :class="{ 'ywicon-unfold': !item.stepIsShow, 'ywicon-fold': item.stepIsShow }"
                                             ></span>
                                          </div>
                                       </div>
                                       <!-- #endregion -->
                                       <!-- #region ====================== 过程输出 ======================-->
                                       <el-steps v-show="item.stepIsShow" class="mt-3" direction="vertical" :active="activeStep">
                                          <el-step
                                             :key="`template-${index}`"
                                             v-for="(subItem, index) in item.stepList"
                                             :title="subItem.title"
                                             :status="stepEnumMap[subItem.status]"
                                          >
                                             <template
                                                #icon
                                                v-if="index + 1 === item.stepList.length && isTalking && msgIndex === computedMessageList.length - 1"
                                             >
                                                <span class="ywifont ywicon-loading1 animate-spin !text-[24px]"></span>
                                             </template>
                                             <template #title>
                                                <span class="">
                                                   {{ subItem.title }}
                                                   <span v-if="subItem.ms" class="text-green-600">{{ `(${subItem.ms})` }}</span></span
                                                >
                                             </template>
                                             <template #description v-if="subItem?.subStep?.length > 0">
                                                <div class="my-1 flex flex-col gap-1 text-[14px]">
                                                   <div
                                                      :key="`${item.historyId}-${index + 1}-${multiChatIndex + 1}`"
                                                      v-for="(multiChatItem, multiChatIndex) in subItem.subStep"
                                                   >
                                                      <component
                                                         v-if="multiChatItem.type === MultiChatType.Select"
                                                         :order="`${index + 1}-${multiChatIndex + 1}`"
                                                         :item="multiChatItem"
                                                         :is="multiChatTypeMapCom[multiChatItem.type]"
                                                         :disabled="
                                                            !(
                                                               index + 1 === item.stepList.length &&
                                                               isTalking &&
                                                               msgIndex === computedMessageList.length - 1
                                                            )
                                                         "
                                                      />
                                                      <component
                                                         v-else-if="multiChatItem.type === MultiChatType.Result"
                                                         :is="answerTypeMapCom['summary']"
                                                         :data="multiChatItem.data.content.values"
                                                         :originData="multiChatItem.data"
                                                      />
                                                      <div v-else-if="multiChatItem.type === MultiChatType.Summary" 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 multiChatItem.data.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>
                                                </div>
                                             </template>
                                          </el-step>
                                       </el-steps>
                                       <!-- #endregion -->
                                    </div>
                                    <!-- #endregion -->
                                    <!-- #region ====================== 消息内容 ======================-->
                                    <template v-if="item.content?.values">
                                       <!-- #region ====================== 报错信息 ======================-->
                                       <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-3 flex" v-if="showFixQuestion(item)">
                                             <div class="text-gray-600 flex-0 mb-auto py-3">
                                                {{ '猜你想问:' }}
                                             </div>
                                             <div class="flex-auto space-x-2 space-y-1 inline-flex flex-wrap items-center">
                                                <div
                                                   v-for="fixItem in item.content.origin?.sample_question"
                                                   :key="fixItem"
                                                   class="bg-gray-200 p-3 hover:bg-[#c5e0ff] hover:text-[#1c86ff] cursor-pointer rounded-lg first-of-type:ml-2 first-of-type:mt-1"
                                                   @click="fixQuestionClick(fixItem, item.content.origin)"
                                                >
                                                   {{ fixItem }}
                                                </div>
                                             </div>
                                          </div>
                                       </div>
                                       <!-- #endregion -->
                                       <!-- #region ====================== 回答组件 ======================-->
                                       <template v-else>
                                          <component
                                             :conclusion="item.conclusion"
                                             :is="answerTypeMapCom[item.content.type]"
                                             :data="item.content.values"
                                             :originData="item"
                                             :isTalking="isTalking && msgIndex === computedMessageList.length - 1"
                                          />
                                          <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>
                                       <!-- #endregion -->
                                    </template>
                                    <!-- #endregion -->
                                    <!-- #region ====================== 附加内容 ======================-->
                                    <!-- #region ====================== 停止 ======================-->
                                    <span v-if="item.isStopMsg && item?.role === RoleEnum.assistant" class="text-gray-400 text-[12px]"
                                       >(已停止)</span
                                    >
                                    <!-- parseContent 返回为 null -->
                                    <p v-if="!item.content && !isTalking && !item.isStopMsg" class="text-red-500">暂无数据</p>
                                    <!-- #endregion -->
                                    <!-- #endregion -->
                                 </div>
                                 <!-- #region ====================== ai 消息操作 ======================-->
                                 <div
                                    v-if="item.role === RoleEnum.assistant && item.content.origin?.ext_call_list"
                                    class="flex font-bold items-center mt-6"
                                    v-if="item.role === RoleEnum.assistant && item.content?.values && !isSharePage && !isShareCheck"
                                    class="absolute flex items-center right-0 mr-4 mt-2 space-x-2"
                                 >
                                    <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
                                       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">
                                       <el-tooltip effect="dark" content="点赞" placement="top">
                                          <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>
                                       </el-tooltip>
                                       <el-tooltip effect="dark" content="点踩" placement="top">
                                          <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>
                                       </el-tooltip>
                                    </template>
                                    <el-tooltip effect="dark" content="分享" placement="top">
                                       <div class="flex items-center justify-center size-[15px]">
                                          <i
                                             class="p-2 ywifont ywicon-fenxiang cursor-pointer hover:text-[#0284ff] !text-[15px] hover:!text-[18px]"
                                             @click="shareClick(item)"
                                          />
                                       </div>
                                    </el-tooltip>
                                    <el-tooltip effect="dark" content="反馈" placement="top">
                                       <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>
                                    </el-tooltip>
                                 </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"
                                 />
                                 <!-- #endregion -->
                              </div>
                           </div>
                        </div>
                        <Loding v-if="isTalking && index === messageList.length - 1" class="w-fit" :process="process" />
                     </div>
                  </div>
               </div>
               <div v-if="showAskMore" class="ml-4 mt-5 text-sm">
               </template>
               <el-empty v-else-if="isSharePage && !chatListLoading" :image-size="200">
                  <template #description>
                     <span class="text-[15px]"> 分享的对话不存在或已失效 </span>
                  </template>
               </el-empty>
               <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
@@ -138,58 +280,125 @@
               </div>
            </div>
         </div>
         <div class="absolute right-28 bottom-40" v-if="!isBottom">
            <div
               class="flex items-center justify-center size-[38px] cursor-pointer hover:text-[#0284ff] border rounded-full hover:bg-[#f6f7f9] shadow bg-white"
               @click="scrollToBottom"
            >
               <i class="ywifont ywicon-xiangxiajiantou !text-[20px]" />
            </div>
         </div>
         <div class="sticky bottom-0 w-full p-6 pb-8 bg-[rgb(247,248,250)] flex justify-center">
         <div
            class="sticky bottom-0 w-full px-6 pt-12 pb-6 bg-[rgb(247,248,250)] flex justify-center"
            v-if="!isSharePage && !isShareCheck"
         >
            <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"
            ></PlayBar>
         </div>
         <div class="sticky bottom-0 w-full p-6 bg-[rgb(247,248,250)] flex justify-center" v-if="isShareCheck"></div>
      </div>
      <CustomDrawer v-model:isShow="drawerIsShow" @updateChatInput="updateChatInput" />
      <!-- <el-dialog title="分享链接" v-model="shareCodeIsShow" width="12%" modal-append-to-body lock-scroll :before-close="closeShareClick">
         <div class="w100 h100 flex justify-center items-center flex-col text-center">
            <div class="qrcode h100" ref="qrcodeRef"></div>
         </div>
      </el-dialog> -->
      <ShareLinkDlg ref="shareLinkDlgRef" />
   </div>
</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 type { CancelTokenSource } from 'axios';
import axios from 'axios';
import { findLast, orderBy } from 'lodash-es';
import moment from 'moment';
import { computed, onActivated, onMounted, ref } from 'vue';
import useClipboard from 'vue-clipboard3';
import { loadAmisSource } from '../amis/load';
import FeedbackPanel from './components/FeedbackPanel.vue';
import Loding from './components/Loding.vue';
import { useAssistantContentOpt } from './hooks/useAssistantContentOpt';
import { useQueryProcess } from './hooks/useQueryProcess';
import { convertProcessItem, useScrollLoad } from './hooks/useScrollLoad';
import { useScrollToBottom } from './hooks/useScrollToBottom';
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 type { ChatContent, StepItem } from './model/types';
import {
   AnswerState,
   AnswerType,
   MultiChatType,
   RoleEnum,
   answerTypeMapCom,
   roleImageMap,
   stepEnumMap,
   type ChatMessage,
} from './model/types';
import { extCallQuery, 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 ShareLinkDlg from './components/shareLink/index.vue';
import UserMsg from './user/index.vue';
import { multiChatTypeMapCom } from '/@/components/chat/chatComponents/multiChat';
import router from '/@/router';
import {
   activeChatRoom,
   activeGroupType,
   activeLLMId,
   activeRoomId,
   activeSampleId,
   activeSectionAId,
   getRoomConfig,
   isSharePage,
   roomConfig,
} from '/@/stores/chatRoom';
import emitter from '/@/utils/mitt';
import { ErrorCode } from '/@/utils/request';
import { useCompRef } from '/@/utils/types';
import { toMyFixed } from '/@/utils/util';
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;
activeRoomId.value = currentRouteId;
const chatListDom = ref<HTMLDivElement>();
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) => {
   if (!res) return null;
   let content: ChatContent = {
      type: AnswerType.Text,
@@ -220,7 +429,10 @@
      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,22 +454,53 @@
         };
         break;
   }
   content.askMoreList = _.orderBy(res.context_history, [(item) => Number(item.radio)], ['desc']);
   content.askMoreList = orderBy(res.context_history, [(item) => Number(item.radio)], ['desc']);
   content.errCode = res?.err_code;
   content.errMsg = res?.json_msg;
   content.origin = res;
   return content;
};
const { clearQueryProcess, process, processId, queryProcess } = useQueryProcess();
//#region ====================== 步骤 step ======================
const activeStep = ref(-1);
const stepList = ref<StepItem[]>([
   {
      title: '意图分析中...',
      status: 0,
   },
   {
      title: '意图分析完成',
      status: 1,
   },
   {
      title: '思考如何执行:指标明细查询',
      status: 1,
   },
   {
      title: '指标明细查询完成',
      status: 1,
   },
]);
const resetStep = () => {
   activeStep.value = -1;
   stepList.value = [];
};
const toggleStepLabel = (item: ChatMessage) => (item.stepIsShow ? '收起' : '展开');
const toggleStepList = (item: ChatMessage) => {
   item.stepIsShow = !item.stepIsShow;
};
//#endregion
const DEFAULT_SECTION_A_ID = 'knowledge_base';
let questionRes = null;
let position = null;
let finalCalcSectionAId = null;
const questionAi = async (text) => {
   processId.value = uuidv4();
let lastAxiosSource: CancelTokenSource = null;
const questionAi = async (text) => {
   let judgeParams = null;
   if (!preQuestion.value) {
      // const aiContent = computedMessageList.value.filter((item) => item.role === RoleEnum.assistant);
@@ -267,7 +510,7 @@
      //          prev_question: lastQuestion,
      //      }
      //    : {};
         // 正常回答暂时不采用
      // 正常回答暂时不采用
      judgeParams = {};
   } else {
      judgeParams = {
@@ -279,20 +522,33 @@
      currentSectionAId = activeSectionAId.value;
      activeSectionAId.value = '';
   } else {
      const lastSectionAItem = _.findLast(computedMessageList.value as any, (item) => item.role === RoleEnum.assistant && !!item.sectionAId);
      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,
      // process_id: processId.value,
      question: text,
      // FIXME: 暂时这样
      section_a_id: currentSectionAId,
      // 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;
@@ -302,13 +558,127 @@
   // if (currentLLMId) {
   //    params.llm_id = currentLLMId;
   // }
   clearQueryProcess();
   queryProcess();
   const res = await QuestionAi(params).finally(() => {
      clearQueryProcess();
   // clearQueryProcess();
   // queryProcess();
   resetStep();
   let lastTimestamp = new Date().getTime();
    questionRes = {};
   const resultP = new Promise((resolve, reject) => {
      const currentSource = axios.CancelToken.source();
      lastAxiosSource = currentSource;
      questionStreamByPost(
         params,
         (chunkRes) => {
            Logger.info('chunk response:\n\n' + JSON.stringify(chunkRes));
            if (chunkRes.mode === 'result') {
               const res = chunkRes.value;
               questionRes = res;
               resolve(res);
               chunkRes.value = '准备数据分析';
               // 将 summary 添加到 subStep 中
               // if (chunkRes.value?.summary) {
               //    const stepList = computedMessageList.value.at(-1).stepList;
               //    const lastStepItem = stepList.at(-1);
               //    if (!lastStepItem.subStep) {
               //       lastStepItem.subStep = [];
               //    }
               //    lastStepItem.subStep.push({
               //       type: MultiChatType.Result, // 新增一个 summary 类型
               //       data: { content: parseContent(chunkRes.value) },
               //    });
               //    scrollToBottom();
               // }
               // return;
            }
            if (chunkRes.mode === 'summary') {
               const lastMsg = computedMessageList.value.at(-1);
               const extraContent = parseExtraContent(chunkRes.value);
               // 此对话已经假如到对话列表
               if (lastMsg.content && 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;
                        }
                     }
                  }
               }
               // 此对话还未假如到对话列表
               if (!lastMsg.content && questionRes) {
                  questionRes = {
                     ...questionRes,
                     ...chunkRes.value,
                  };
               }
               // computedMessageList.value[computedMessageList.value.length - 1] = finalMsg;
               scrollToBottom();
               // chunkRes.value = '你可以继续问我';
               return;
            }
            if (chunkRes.mode === 'conclusion') {
               computedMessageList.value.at(-1).conclusion = chunkRes.value;
               chunkRes.value = '分析结束';
            }
            if (chunkRes.mode === 'question') {
               const stepList = computedMessageList.value.at(-1).stepList;
               const lastStepItem = stepList.at(-1);
               if (!lastStepItem.subStep) {
                  lastStepItem.subStep = [];
               }
               lastStepItem.subStep.push({
                  type: chunkRes.value.type,
                  data: chunkRes.value,
               });
               scrollToBottom();
               return;
            }
            const stepList = computedMessageList.value.at(-1).stepList;
            const currentTimeStamp = new Date().getTime();
            const ms = toMyFixed(currentTimeStamp - lastTimestamp, 2) + ' ms';
            if (chunkRes.mode === 'finish') {
               stepList.at(-1).ms = ms;
               return;
            }
            if (stepList?.length >= 1) {
               stepList.at(-1).ms = ms;
            }
            lastTimestamp = currentTimeStamp;
            const stepItem = convertProcessItem(chunkRes);
            stepList.push(stepItem);
            if (chunkRes.mode !== 'result') {
               scrollToBottom();
            }
         },
         {
            cancelToken: currentSource.token,
         }
      )
         .catch((err) => {
            throw err;
         })
         .finally(() => {
            isTalking.value = false;
            computedMessageList.value.at(-1).stepIsShow = false;
            resetStep();
         });
   });
   questionRes = res;
   const content = parseContent(res);
   await resultP;
   // isTalking.value = false;
   const content = parseContent(questionRes, true);
   return content;
};
@@ -322,14 +692,24 @@
let currentLLMId = null;
const getAnswerById = async (historyId: string) => {
   return await GetHistoryAnswer({
      history_id: historyId,
   });
const stopGenClick = () => {
   lastAxiosSource?.cancel();
   isTalking.value = false;
   chatListLoading.value = false;
   resetStep();
   computedMessageList.value.at(-1).isStopMsg = true;
};
const sendChatMessage = async (content: ChatContent = messageContent.value, cb?: any, isCallExtParams?: any) => {
   if (!content?.values || isTalking.value) return;
   if (!content?.values) {
      return;
   }
   if (isTalking.value || chatListLoading.value) {
      ElMessage.warning('ai 正在回复中,请稍后尝试提问');
      return;
   }
   // position = position ?? (await getCurrentPosition());
   const isNewChat = messageList.value.length === 0;
   if (isNewChat) {
      if (activeSampleId.value) {
@@ -339,15 +719,21 @@
      if (activeLLMId.value) {
         currentLLMId = activeLLMId.value;
      }
   }
   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;
      const userItem: ChatMessage = { role: RoleEnum.user, content, isChecked: false } as any;
      const assistantItem: ChatMessage = {
         role: RoleEnum.assistant,
         content: null,
         state: AnswerState.Null,
         stepList: [],
         stepIsShow: true,
         isStopMsg: false,
         isChecked: false,
      } as any;
      // 发送当前
      messageList.value.push(userItem);
      // 清空输入框
@@ -355,32 +741,38 @@
      // 出现回复,置空出现等待动画
      messageList.value.push(assistantItem);
      // 滚动至当前发送消息
      scrollToBottom();
      if (isCallExtParams) {
         const extRes = await extCallQuery(isCallExtParams);
         questionRes = extRes;
         resMsgContent = parseContent(extRes);
         resMsgContent = parseContent(extRes, true);
      } else {
         resMsgContent = await questionAi(content.values);
      }
      nextUserMsgEndIndex.value++;
      if (isNewChat) {
         const firstResCb = getRoomConfig(currentRouteId, 'firstResCb');
         firstResCb?.(resMsgContent);
      } else {
         cb?.(resMsgContent);
      }
      userItem.historyId = questionRes.history_id;
      userItem.historyId = questionRes?.history_id;
      userItem.content.values = questionRes?.question ?? userItem.content.values;
      assistantItem.historyId = questionRes.history_id;
      assistantItem.historyId = questionRes?.history_id;
      assistantItem.sectionAId = finalCalcSectionAId;
      appendLastMessageContent(resMsgContent);
      setTimeout(() => {
         // 收到回复,继续滚
         scrollToBottom();
      }, 300);
   } catch (error: any) {
      console.log("🚀 ~ error:", error)
      // appendLastMessageContent({
      //    type: AnswerType.Text,
      //    values: '发生错误!',
      // });
   } finally {
      isTalking.value = false;
   }
};
@@ -388,71 +780,67 @@
   sendChatMessage(messageContent.value, cb);
};
const appendLastMessageContent = (content: ChatContent) => {
   const currentTime = moment().format('MM月DD日 HH:mm:ss');
   if (messageList.value.at(-1)) {
      messageList.value.at(-1).content = content;
      messageList.value.at(-1).createTime = currentTime;
   }
};
const { loadRangeData, onChatListScroll, moreIsLoading, nextUserMsgEndIndex } = useScrollLoad({
   container: chatListDom,
   historyGroupId: currentRouteId,
   messageList,
   parseAnswerContent: parseContent,
});
const chatListLoading = ref(true);
const { scrollToBottom, scrollToTop, isBottom } = useScrollToBottom({
   chatListDom: chatListDom,
});
onActivated(() => {
   emitter.emit('updateHeaderTitle', activeChatRoom.value?.title ?? '');
});
onMounted(async () => {
   const res = await QueryHistoryDetail({
      history_group_id: currentRouteId,
   messageList.value = [];
   // 加载初始数据
   chatListLoading.value = true;
   await loadRangeData().finally(() => {
      chatListLoading.value = false;
   });
   messageList.value = (res.details ?? []).map((item) => {
      return {
         historyId: item.history_id,
         role: RoleEnum.user,
         content: {
            type: AnswerType.Text,
            values: item.question,
         },
      } as ChatMessage;
   });
   const sectionAIdMap = new Map();
   const resList = await Promise.all(
      (res.details ?? []).map((item) => {
         sectionAIdMap.set(item.history_id, item.section_a_id);
         return getAnswerById(item.history_id);
      })
   );
   let i = 0;
   resList.map((item, index) => {
      const insertIndex = index + 1 + i;
      const userMsg = messageList.value[insertIndex - 1];
      userMsg.content.values = item?.answer?.question ??userMsg.content.values;
      messageList.value.splice(
         insertIndex,
         0,
         item.answer === null
            ? null
            : {
                  historyId: item.answer?.history_id,
                  role: RoleEnum.assistant,
                  content: parseContent(item.answer),
                  state: item.answer_state,
                  sectionAId: sectionAIdMap.get(item.answer.history_id),
              }
      );
      i++;
   });
   setTimeout(() => {
      emitter.emit('updateHeaderTitle', activeChatRoom.value?.title ?? '');
   }, 300);
   if (messageList.value.length === 0) {
      messageContent.value = {
         type: AnswerType.Text,
         values: activeChatRoom.value.title,
         values: activeChatRoom.value?.title,
      };
      sendChatMessage();
   } else {
      if (isSharePage.value) {
         // setTimeout(() => {
         //    // 滚动到顶部
         //    scrollToTop();
         // }, 300);
      } else {
         setTimeout(() => {
            // 初始状态滚一下
            scrollToBottom();
            setTimeout(() => {
               chatListDom.value.addEventListener('scroll', onChatListScroll);
            }, 300);
         }, 300);
      }
   }
   loadAmisSource();
});
const { forbidScroll } = useScrollToBottom({
   chatListDom: chatListDom,
   displayMessageList: computedMessageList,
});
//#region ====================== 关联查询 ======================
const relativeQueryClick = async (val) => {
   sendChatMessage(
@@ -470,7 +858,37 @@
   );
};
//#endregion
//#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,
@@ -487,7 +905,6 @@
   showFixQuestion,
   showAskMore,
} = useAssistantContentOpt({
   forbidScroll,
   sendChatMessage,
   displayMessageList: computedMessageList,
});
@@ -499,12 +916,65 @@
   messageContent.value.values = content;
};
//#endregion
//#region ====================== 用户询问的问题设置为常用语 ======================
const setCommonQuestionInfo = ref({});
const { toClipboard } = useClipboard();
//用户复制问题
const copyUserClick = (item) => {
   const text = item.content.values;
   ElMessage.success('复制成功');
   toClipboard(text);
};
//用户问题设置为常用语
const setCommonQuestionClick = (item) => {
   setCommonQuestionInfo.value = item;
};
//#endregion
//#region ====================== 分享 ======================
const shareLinkDlgRef = useCompRef(ShareLinkDlg);
const isShareCheck = ref(false);
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>