wujingjing
2025-04-09 dd58c1d3a27ba48a5df050aab7c586bb9b988914
src/components/chat/Chat.vue
@@ -1,310 +1,17 @@
<template>
   <ChatContainer
      :loading="chatListLoading"
      :more-is-loading="moreIsLoading"
      :is-share-page="isSharePage"
      :is-share-check="isShareCheck"
      :chat-width="chatWidth"
      ref="containerRef"
   >
   <ChatContainer :loading="chatListLoading" :more-is-loading="moreIsLoading" :is-share-page="isSharePage" 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"
                  @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>
                  <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 ====================== 消息内容 ======================-->
                              <!-- <template v-if="item.content?.values"> -->
                              <!-- #region ====================== 报错信息 ======================-->
                              <div v-if="item.content?.errCode === ErrorCode.Message" class="flex-column w-full">
                                 <p class="text-danger">
                                    {{ 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>
                                 <template v-if="item.content.type === AnswerType.Report">
                                    <template v-if="item?.stepGroup?.length > 0">
                                       <div v-for="(num, index) in item?.stepGroup?.length" :key="index">
                                          <!-- #region ====================== 意图分析 ======================-->
                                          <div class="flex flex-col" v-if="item?.stepGroup?.[index]?.value?.length > 0">
                                             <!-- #region ====================== 意图分析 ======================-->
                                             <div class="flex items-center">
                                                <span class="mr-2">意图分析:</span>
                                                <div
                                                   @click="toggleStepList(item?.stepGroup?.[index])"
                                                   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?.stepGroup?.[index]) }}
                                                   </span>
                                                   <span
                                                      class="ywifont"
                                                      :class="{
                                                         'ywicon-unfold': !item?.stepGroup?.[index].isShow,
                                                         'ywicon-fold': item?.stepGroup?.[index].isShow,
                                                      }"
                                                   ></span>
                                                </div>
                                             </div>
                                             <!-- #endregion -->
                                             <!-- #region ====================== 过程输出 ======================-->
                                             <el-steps
                                                v-show="item?.stepGroup?.[index].isShow"
                                                class="mt-3"
                                                direction="vertical"
                                                :active="activeStep"
                                             >
                                                <el-step
                                                   :key="`template-${stepIndex}`"
                                                   v-for="(subItem, stepIndex) in item?.stepGroup?.[index].value"
                                                   :title="subItem.title"
                                                   :status="stepEnumMap[subItem.status]"
                                                >
                                                   <template
                                                      #icon
                                                      v-if="
                                                         stepIndex + 1 === item?.stepGroup?.[index].value.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}-${stepIndex + 1}-${multiChatIndex + 1}`"
                                                            v-for="(multiChatItem, multiChatIndex) in subItem.subStep"
                                                         >
                                                            <component
                                                               v-if="multiChatItem.type === MultiChatType.Select"
                                                               :order="`${stepIndex + 1}-${multiChatIndex + 1}`"
                                                               :item="multiChatItem"
                                                               :is="multiChatTypeMapCom[multiChatItem.type]"
                                                               :disabled="
                                                                  !(
                                                                     stepIndex + 1 === item?.stepGroup?.[index].value.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 -->
                                          <component
                                             v-if="item.content?.values?.[index]"
                                             :reportIndex="index"
                                             :conclusion="item.content.values[index].conclusion"
                                             :is="answerTypeMapCom[item.content.values[index].content.type]"
                                             :data="item.content.values[index].content.values"
                                             :originData="item.content.values[index]"
                                             :historyId="item.historyId"
                                             :isTalking="isTalking && msgIndex === computedMessageList.length - 1"
                                          />
                                       </div>
                                    </template>
                                    <p v-else class="text-info">暂无内容,请重试</p>
                                 </template>
                                 <component
                                    v-else
                                    :historyId="item.historyId"
                                    :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?.values && !isSharePage && !isShareCheck"
                              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">
                                 <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>
                           <!-- #endregion -->
                        </div>
                     </div>
                  </div>
               </div>
            </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>
         </template>
         <MessageList
            v-if="computedMessageList?.length > 0"
            :msgList="computedMessageList"
            :isTalking="isTalking"
            @shareClick="shareClick"
            @setCommonQuestionClick="setCommonPhraseClick"
            @sendChatMessage="sendChatMessage"
            @stopGenClick="stopGenClick"
            @askMoreClick="askMoreClick"
         />
         <el-empty v-else-if="isSharePage && !chatListLoading" :image-size="200">
            <template #description>
               <span class="text-[15px]">分享的对话不存在或已失效</span>
@@ -314,18 +21,20 @@
      <!-- 输入区域 -->
      <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"
         />
         <div class="w-full">
            <PlayBar
               ref="playBarRef"
               v-model:voicePageIsShow="voicePageIsShow"
               :isTalking="isTalking"
               :isHome="false"
               :msgList="computedMessageList"
               v-model="messageContent.values"
               @sendClick="sendClick"
               @stopGenClick="stopGenClick"
               :style="{ width: chatWidth }"
               class="mx-auto"
            />
         </div>
      </template>
      <!-- 抽屉 -->
@@ -339,36 +48,24 @@
<script setup lang="ts">
import type { CancelTokenSource } from 'axios';
import axios from 'axios';
import { findLast, orderBy } from 'lodash-es';
import { ElMessage } from 'element-plus';
import moment from 'moment';
import { computed, onActivated, onMounted, ref } from 'vue';
import useClipboard from 'vue-clipboard3';
import { v4 as uuidv4 } from 'uuid';
import { computed, nextTick, onActivated, onMounted, ref } from 'vue';
import { loadAmisSource } from '../amis/load';
import FeedbackPanel from './components/FeedbackPanel.vue';
import { useAssistantContentOpt } from './hooks/useAssistantContentOpt';
import { convertProcessItem, useScrollLoad } from './hooks/useScrollLoad';
import { useScrollToBottom } from './hooks/useScrollToBottom';
import ChatContainer from './components/ChatContainer.vue';
import ShareLinkDlg from './components/shareLink/index.vue';
import type { SendMsg } from './hooks/types';
import { useLoadData } from './hooks/useLoadData';
import { useScrollLoad } from './hooks/useScrollLoad';
import { useSyncMsg } from './hooks/useSyncMsg';
import MessageList from './messageList/index.vue';
import type { ChatContent } from './model/types';
import {
   AnswerState,
   AnswerType,
   MultiChatType,
   RoleEnum,
   answerTypeMapCom,
   roleImageMap,
   stepEnumMap,
   type ChatMessage,
} from './model/types';
import { extCallQuery, questionStreamByPost } 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 { Logger } from '/@/model/logger/Logger';
import { ElMessage } from 'element-plus';
import ChatContainer from './components/ChatContainer.vue';
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,
@@ -376,21 +73,34 @@
   activeLLMId,
   activeRoomId,
   activeSampleId,
   activeSectionAId,
   getRoomConfig,
   isSharePage,
   roomConfig,
} from '/@/stores/chatRoom';
import { ParentRegister } from '/@/stores/global';
import emitter from '/@/utils/mitt';
import { ErrorCode } from '/@/utils/request';
import { deepClone } from '/@/utils/other';
import { useCompRef } from '/@/utils/types';
import { toMyFixed } from '/@/utils/util';
const containerRef = ref();
import { toFormData, toMyFixed } from '/@/utils/util';
const containerRef = useCompRef(ChatContainer);
const chatListDom = computed(() => containerRef.value?.chatListDom);
const chatWidth = '75%';
const scrollToBottom = () => {
   containerRef.value?.scrollToBottom();
};
const {
   loadReplyData,
   parseContent,
   parseExtraContent,
   convertProcessItem,
   convertProcessToStep,
   convertAttach,
   formatShowTimeYear,
   getStepGroupList,
} = useLoadData();
const voicePageIsShow = ref(false);
let isTalking = ref(false);
const chatWidth = computed(() => containerRef.value?.chatWidth);
let messageContent = ref<ChatContent>({
   type: AnswerType.Text,
   values: '',
@@ -403,131 +113,24 @@
   return messageList.value.filter((v) => !!v);
});
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:
         content = {
            type: AnswerType.RecordSet,
            values: res.values,
         };
         break;
      case AnswerType.Text:
         content = {
            type: AnswerType.Text,
            values: res.values ?? res.answer,
         };
         break;
      case AnswerType.Script:
         content = {
            type: AnswerType.Script,
            values: res,
         };
         break;
      case AnswerType.Knowledge:
         content = {
            type: AnswerType.Knowledge,
            values: res.knowledge,
         };
         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?.map((item) => {
               item.reportIsShow = reportIsShow;
               return item;
            }),
         };
         break;
      case AnswerType.Url:
         content = {
            type: AnswerType.Url,
            values: res.url,
         };
         break;
      case AnswerType.Map:
         content = {
            type: AnswerType.Map,
            values: res.values,
         };
         break;
      default:
         content = {
            type: AnswerType.Text,
            values: '解析失败!',
         };
         break;
   }
   if (!extraContent) {
      content = {
         ...content,
         ...curExtraContent,
      };
   } else {
      content = {
         ...content,
         ...extraContent,
      };
   }
   return content;
};
//#region ====================== 步骤 step ======================
const activeStep = ref(-1);
const resetStep = () => {
   activeStep.value = -1;
};
const toggleStepLabel = (item: any) => (item.isShow ? '收起' : '展开');
const toggleStepList = (item: any) => {
   item.isShow = !item.isShow;
};
//#endregion
const DEFAULT_SECTION_A_ID = 'knowledge_base';
let questionRes = null;
let position = null;
let finalCalcSectionAId = null;
const preQuestion = ref(null);
let lastAxiosSource: CancelTokenSource = null;
// 通过修改 isTalking 来触发更新
const triggerRefresh = () => {
   isTalking.value = !isTalking.value;
   nextTick(() => {
      isTalking.value = !isTalking.value;
   });
};
const enableCallback = ref(false);
let streamOutputIsStart = false;
let position: Position = null;
const questionAi = async (text) => {
   let judgeParams = null;
   if (!preQuestion.value) {
@@ -537,18 +140,6 @@
         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 = {
      question: text,
@@ -556,12 +147,30 @@
      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(',');
   const tableList = attachList.value.filter((item) => item.type === 'table').map((item) => item.model);
   if (tableList?.length > 0) {
      params.tables = JSON.stringify(tableList);
   }
   const metricList = attachList.value.filter((item) => item.type === 'metric').map((item) => item.model);
   if (metricList?.length > 0) {
      params.metrics = JSON.stringify(metricList);
   }
   // if (!position) {
   //    const loadingInstance = ElLoadingService({
   //       text: '获取位置中...',
   //       target: '.layout-parent',
   //       fullscreen:false,
   //    });
   //    position = await getCurrentPosition().finally(() => {
   //       loadingInstance.close();
   //    });
   // }
   // if (position) {
   //    const { latitude, longitude } = position;
   //    params.cur_pos = [longitude, latitude].join(',');
   // }
   if (activeGroupType.value) {
      params.group_type = activeGroupType.value;
@@ -571,8 +180,12 @@
      params.sample_id = currentSampleId;
      currentSampleId = '';
   }
   resetStep();
   const formDataParams = toFormData(params);
   const fileList = attachList.value.filter((item) => item.type === 'file').map((item) => item.model);
   for (const item of fileList) {
      formDataParams.append('files', item.file);
   }
   // clearAttach();
   let lastTimestamp = new Date().getTime();
   questionRes = {};
   let lastIsResult = false;
@@ -589,35 +202,58 @@
      };
      const checkReportEmpty = () => {
         const isEmpty = !questionRes?.reports || questionRes?.reports?.length === 0;
         return isEmpty;
      };
      questionStreamByPost(
         params,
         formDataParams,
         (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;
                  // resReport.reports = resReport.reports.concat([]);
                  resolve(resReport);
               } else {
                  const lastMsg = computedMessageList.value.at(-1);
                  // lastMsg.content.values = lastMsg.content.values.concat([]);
                  // 已经解析过一次 reports
                  if (!lastMsg.content.values) {
                     lastMsg.content.values = [];
                  }
                  lastMsg.content.values.push({
                     content: parseContent(res, true, {
                        origin: res,
                     }),
                  });
               }
               triggerRefresh();
               return;
               // chunkRes.value = '准备数据分析';
            }
            if (chunkRes.mode === 'main_frame') {
               const jsonObj = JSON.parse(chunkRes.value);
               if (!enableCallback.value) {
                  return;
               }
               ParentRegister.notify?.({
                  type: 'main_frame',
                  value: jsonObj,
               });
               return;
            }
            if (chunkRes.mode === 'create_work_order') {
               const lastMsg = computedMessageList.value.at(-1);
               lastMsg.modeContent = chunkRes;
               triggerRefresh();
               return;
            }
            if (chunkRes.mode === 'summary') {
@@ -668,21 +304,36 @@
            }
            if (chunkRes.mode === 'conclusion') {
               const lastReport = computedMessageList.value.at(-1)?.content?.values?.at(-1);
               if(lastReport){
               if (lastReport) {
                  lastReport.conclusion = chunkRes.value;
                  chunkRes.value = '分析结束';
               }
               chunkRes.value = '分析结束';
            }
            const getLastGroup = () => {
               const lastGroup = computedMessageList.value.at(-1).stepGroup[0];
               return lastGroup;
            };
            const getLastStepList = () => {
               const stepList = getLastGroup()?.value ?? [];
               return stepList;
            };
            const getLastStepItem = () => {
               const stepList = getLastStepList();
               const lastStepItem = stepList.at(-1);
               return lastStepItem;
            };
            const checkStepItem = (stepItem) => {
               if (!stepItem.subStep) {
                  stepItem.subStep = [];
               }
            };
            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 = [];
               }
               const lastStepItem = getLastStepItem();
               checkStepItem(lastStepItem);
               lastStepItem.subStep.push({
                  type: chunkRes.value.type,
                  data: chunkRes.value,
@@ -690,40 +341,59 @@
               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);
            // 暂时不考虑多个 report 情况
            if (lastIsResult && chunkRes.mode !== 'finish') {
               // const lastTow = computedMessageList.value.at(-1);
               // lastTow.stepGroup.at(-1).value.at(-1).finishLoading = true;
               // lastTow.content.values = lastTow.content.values.concat([]);
               // 开始增加新的 stepGroup,后续的 stepGroup 并没有实际作用,只是为了做迭代用,迭代出组件,屎山代码实在太难改了!!!
               computedMessageList.value.at(-1).stepGroup.push({
                  value: [],
                  isShow: true,
               });
               lastIsResult = false;
            }
            const lastGroup = getLastGroup();
            const stepList = lastGroup?.value ?? [];
            const currentTimeStamp = new Date().getTime();
            const ms = toMyFixed(currentTimeStamp - lastTimestamp, 2) + ' ms';
            if (chunkRes.mode === 'finish') {
               const ms = toMyFixed(currentTimeStamp - lastTimestamp, 2) + ' ms';
               stepList.at(-1).ms = ms;
               isTalking.value = false;
               streamOutputIsStart = false;
               return;
            }
            if (stepList?.length >= 1) {
            if (stepList?.length >= 1 && !streamOutputIsStart) {
               const ms = toMyFixed(currentTimeStamp - lastTimestamp, 2) + ' ms';
               stepList.at(-1).ms = ms;
            }
            if (!streamOutputIsStart) {
               lastTimestamp = currentTimeStamp;
            }
            if (!streamOutputIsStart) {
               const stepItem = convertProcessItem(chunkRes);
               stepList.push(stepItem);
            } else {
               const stepGroup = computedMessageList.value.at(-1).stepGroup;
               if (stepGroup.length > 1) {
                  const lastStepList = stepGroup.at(-2).value;
                  lastStepList.at(-1).ms = ms;
               const lastItem = stepList.at(-1);
               if (lastItem) {
                  lastItem.title += chunkRes.value ?? '';
               }
            }
            lastTimestamp = currentTimeStamp;
            const stepItem = convertProcessItem(chunkRes);
            stepList.push(stepItem);
            if (chunkRes.mode === 'begin_stream') {
               streamOutputIsStart = true;
               lastTimestamp = currentTimeStamp;
            }
            if (chunkRes.mode === 'end_stream') {
               streamOutputIsStart = false;
            }
            // 强制触发更新
            scrollToBottom();
@@ -741,8 +411,6 @@
            computedMessageList.value.at(-1).stepGroup.forEach((item) => {
               item.isShow = false;
            });
            resetStep();
         });
   });
@@ -750,34 +418,139 @@
   const content = parseContent(questionRes, true);
   return content;
};
const clearMessageContent = () =>
   (messageContent.value = {
const playBarRef = useCompRef(PlayBar);
const attachList = computed(() => playBarRef.value?.attachList ?? []);
const clearMessageContent = () => {
   messageContent.value = {
      type: AnswerType.Text,
      values: '',
   });
   };
};
let currentSampleId = '';
let currentLLMId = null;
const stopGenClick = () => {
const resetTalking = () => {
   lastAxiosSource?.cancel();
   isTalking.value = false;
   chatListLoading.value = false;
   resetStep();
   streamOutputIsStart = false;
};
const stopGenClick = () => {
   resetTalking();
   if (isFrontQuestion) {
      ParentRegister.notify?.({
         type: 'msg_stop',
      });
   }
   computedMessageList.value.at(-1).isStopMsg = true;
};
const sendChatMessage = async (content: ChatContent = messageContent.value, cb?: any, isCallExtParams?: any) => {
const finishFrontQuestion = () => {
   resetTalking();
   ParentRegister.updateChildCallObj('sendMsg', null);
};
const checkCanSend = (content: ChatContent = messageContent.value) => {
   if (!content?.values) {
      return;
      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, attachList: deepClone(attachList.value) } as any;
   const assistantItem: ChatMessage = {
      role: RoleEnum.assistant,
      content: {
         type: AnswerType.Report,
         values: [],
      },
      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 clearAttach = () => {
   playBarRef.value?.clearAttach();
};
const updateUserInfo = (userItem: ChatMessage, other: { historyId: string; question: string }) => {
   userItem.historyId = other.historyId;
   const current = moment().format('YYYY-MM-DD HH:mm:ss');
   userItem.createTime = current;
   userItem.content.values = other.question ?? userItem.content.values;
};
const updateAssistantInfo = (
   assistantItem: ChatMessage,
   resMsgContent: ChatContent,
   other: { historyId: string; question: string }
) => {
   const current = moment().format('YYYY-MM-DD HH:mm:ss');
   assistantItem.historyId = other.historyId;
   const currentTime = formatShowTimeYear(current);
   assistantItem.createTime = currentTime;
   assistantItem.content = resMsgContent;
};
const updateInfo = (
   userItem: ChatMessage,
   assistantItem: ChatMessage,
   resMsgContent: ChatContent,
   other: {
      historyId: string;
      question: string;
   }
) => {
   updateUserInfo(userItem, other);
   updateAssistantInfo(assistantItem, resMsgContent, other);
};
const handleAfterQuestion = (
   userItem: ChatMessage,
   assistantItem: ChatMessage,
   resMsgContent: ChatContent,
   other: { historyId: string; question: string }
) => {
   updateLoadIndex();
   updateInfo(userItem, assistantItem, resMsgContent, other);
   setTimeout(() => {
      // 收到回复,继续滚
      scrollToBottom();
   }, 300);
};
const sendChatMessage = async (content: ChatContent = messageContent.value) => {
   if (!checkCanSend(content)) {
      return;
   }
   isFrontQuestion = false;
   const isNewChat = messageList.value.length === 0;
   if (isNewChat) {
      if (activeSampleId.value) {
@@ -791,186 +564,215 @@
   let resMsgContent: ChatContent = null;
   try {
      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();
      if (isCallExtParams) {
         const extRes = await extCallQuery(isCallExtParams);
         questionRes = 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.content.values = questionRes?.question ?? userItem.content.values;
      assistantItem.historyId = questionRes?.history_id;
      assistantItem.sectionAId = finalCalcSectionAId;
      appendLastMessageContent(resMsgContent);
      setTimeout(() => {
         // 收到回复,继续滚
         scrollToBottom();
      }, 300);
      const [userItem, assistantItem] = addChatItem(content);
      resMsgContent = await questionAi(content.values);
      handleAfterQuestion(userItem, assistantItem, resMsgContent, {
         historyId: questionRes?.history_id,
         question: questionRes?.question,
      });
   } catch (error: any) {}
};
let isFrontQuestion = false;
const sendFrontChatMessage = async (content: ChatContent = messageContent.value): Promise<any> => {
   isFrontQuestion = true;
   const [userItem, assistantItem] = addChatItem(content);
   const promise = new Promise((resolve, reject) => {
      const receiveMsg = (msg: SendMsg) => {
         console.log('receiveMsg', msg);
         if (!assistantItem.content?.values) {
            assistantItem.content.values = [];
         }
         switch (msg.type) {
            case 'text':
               // 开始增加新的 stepGroup,后续的 stepGroup 并没有实际作用,只是为了做迭代用,迭代出组件,屎山代码实在太难改了!!!
               assistantItem.stepGroup.push({
                  value: [],
                  isShow: true,
               });
const sendClick = (cb) => {
   sendChatMessage(messageContent.value, cb);
               assistantItem.content.values.push({
                  content: {
                     type: 'knowledge',
                     values: [
                        {
                           answer: msg.value,
                        },
                     ],
                  },
               });
               break;
            case 'select':
            case 'confirm':
            case 'input':
               // 开始增加新的 stepGroup,后续的 stepGroup 并没有实际作用,只是为了做迭代用,迭代出组件,屎山代码实在太难改了!!!
               assistantItem.stepGroup.push({
                  value: [],
                  isShow: true,
               });
               assistantItem.content.values.push({
                  content: {
                     type: 'content_cb',
                     values: msg,
                  },
               });
               break;
            case 'info':
               if (msg.value === 'finish') {
                  finishFrontQuestion();
                  return resolve({ userItem, assistantItem, resMsgContent: assistantItem.content });
               }
               break;
            default:
               break;
         }
         scrollToBottom();
         triggerRefresh();
      };
      ParentRegister.updateChildCallObj('sendMsg', receiveMsg);
   });
   return promise;
};
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 questionSelf = (content: string) => {
   const myContent = { type: AnswerType.Text, values: content };
   if (!checkCanSend(myContent)) {
      return false;
   }
   sendFrontChatMessage(myContent).then(({ userItem, assistantItem, resMsgContent }) => {
      handleAfterQuestion(userItem, assistantItem, resMsgContent, {
         historyId: uuidv4(),
         question: content,
      });
   });
   return true;
};
const { loadRangeData, onChatListScroll, moreIsLoading, nextUserMsgEndIndex } = useScrollLoad({
const backQuestion = (content: { question: string; data: any }) => {
   sendChatMessage({ type: AnswerType.Text, values: content.question });
};
ParentRegister.updateChildCallObj('frontQuestion', questionSelf);
ParentRegister.updateChildCallObj('backQuestion', backQuestion);
const sendClick = () => {
   sendChatMessage(messageContent.value);
};
const { loadRangeData, onChatListScroll, moreIsLoading, updateLoadIndex } = useScrollLoad({
   container: chatListDom,
   historyGroupId: currentRouteId,
   messageList,
   parseAnswerContent: parseContent,
   loadReplyData,
});
useSyncMsg({
   msgList: messageList,
   updateLoadIndex,
   historyGroupId: currentRouteId,
   checkCanSync: (data) => {
      return !isTalking.value && !moreIsLoading.value;
   },
   showTip: (data) => {
      playBarRef.value.showSyncTip(data);
   },
   loadReplyData,
   scrollToBottom,
});
const chatListLoading = ref(true);
const { scrollToBottom, scrollToTop, isBottom } = useScrollToBottom({
   chatListDom: chatListDom,
});
onActivated(() => {
   emitter.emit('updateHeaderTitle', activeChatRoom.value?.title ?? '');
});
const initNewChat = () => {
   messageContent.value = {
      type: AnswerType.Text,
      values: activeChatRoom.value?.title,
   };
   sendChatMessage();
};
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,
      },
      attachList: convertAttach(msgValue),
      isChecked: false,
   };
   const assistantMsg: ChatMessage = {
      historyId: msgValue.history_id,
      role: RoleEnum.assistant,
      content: parseContent(msgValue),
      stepGroup: getStepGroupList(msgValue?.reports ?? []),
      isStopMsg: false,
      conclusion: msgValue.conclusion ?? [],
      isChecked: false,
   };
   messageList.value = [userMsg, assistantMsg];
};
onMounted(async () => {
   messageList.value = [];
   chatListLoading.value = true;
   await loadRangeData().finally(() => {
      chatListLoading.value = false;
   });
   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();
      initNewChat();
   } else {
      if (isSharePage.value) {
         // setTimeout(() => {
         //    // 滚动到顶部
         //    scrollToTop();
         // }, 300);
      } else {
      if (!isSharePage.value) {
         setTimeout(() => {
            // 初始状态滚一下
            scrollToBottom();
            setTimeout(() => {
               chatListDom.value.addEventListener('scroll', onChatListScroll);
            }, 300);
            initHistoryChat();
         }, 300);
      }
   }
   loadAmisSource();
});
//#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,
      }
   );
const askMoreClick = (item) => {
   if (!item.question) return;
   sendChatMessage({ type: AnswerType.Text, values: item.question });
};
//#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,
   unLikeClick,
   feedbackPosition,
   feedbackIsShow,
   feedbackContent,
   feedbackPanelRef,
   currentFeedbackMapItem,
   feedbackClick,
   askMoreClick,
   fixQuestionClick,
   preQuestion,
   showFixQuestion,
   showAskMore,
} = useAssistantContentOpt({
   sendChatMessage,
   displayMessageList: computedMessageList,
});
//#region ====================== 侧边栏drawer ======================
const drawerIsShow = ref(false);
@@ -979,26 +781,14 @@
   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;
const setCommonPhraseClick = (item) => {
   playBarRef.value.addPhrase(item);
};
//#endregion
//#region ====================== 分享 ======================
const shareLinkDlgRef = useCompRef(ShareLinkDlg);
const isShareCheck = ref(false);
const shareClick = async (item: ChatMessage) => {
   shareLinkDlgRef.value.openShare(item);
@@ -1007,37 +797,5 @@
</script>
<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;
}
@import './index.scss';
</style>