wujingjing
2024-11-04 40b0a53cbec991f04cb5d6f3b936a524712ece2c
src/components/chat/Chat.vue
@@ -1,100 +1,241 @@
<template>
   <div class="flex flex-col h-full">
      <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"
               :class="{ 'flex-row-reverse': item.role === RoleEnum.user }"
               v-for="(item, index) of computedMessageList"
               :key="index"
            >
               <img
                  class="rounded-full size-12 mr-4"
                  :class="{ 'mr-4': item.role === RoleEnum.assistant, 'ml-4': item.role === RoleEnum.user }"
                  :src="roleImageMap[item.role]"
                  alt=""
                  srcset=""
               />
   <div class="flex h-full">
      <div class="flex flex-col h-full flex-auto">
         <div ref="chatListDom" class="relative h-full flex flex-col items-center overflow-y-auto">
            <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 }">
               <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="`${item.historyId}_${item.role}`"
               >
                  <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, '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">
                           <div
                              class="text-sm rounded-[6px] p-4 leading-relaxed"
                              :style="{ backgroundColor: item.role === RoleEnum.user ? 'rgb(197 224 255)' : 'white' }"
                           >
                              <div class="flex flex-col" v-if="item?.stepList?.length > 0">
                                 <div class="flex items-center mb-3">
                                    <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>
               <div class="inline-flex flex-col" :class="{ 'w-full': item.role === RoleEnum.assistant }">
                  <div class="relative 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' }"
                     >
                        <component :is="answerTypeMapCom[item.content.type]" :data="item.content.values" :error="item.content.error" />
                     </div>
                                 <!-- 过程输出 -->
                                 <el-steps v-show="item.stepIsShow" direction="vertical" :active="activeStep">
                                    <el-step
                                       v-for="(subItem, index) in item.stepList"
                                       :title="subItem.title"
                                       :status="stepEnumMap[subItem.status]"
                                    >
                                       <!-- <template #icon>
                                       <span v-if="subItem.status === StepEnum.Loading" class="ywifont ywicon-loading animate-spin"></span>
                                       <span v-else class="ywifont ywicon-loading1 animate-spin"></span>
                                    </template> -->
                                       <template #title>
                                          <span class="text-sm">{{ subItem.title }}</span>
                                       </template>
                                    </el-step>
                                 </el-steps>
                              </div>
                     <div v-if="item.role === RoleEnum.assistant" class="absolute flex items-center right-0 mr-2 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 ywicon icon-copy cursor-pointer hover:text-[#0284ff] hover:!text-[18px]" @click="copyClick(item)" />
                        </div>
                        <div class="flex items-center justify-center size-[15px]">
                           <i
                              :class="{ 'text-[#0284ff]': item.state === AnswerState.Like }"
                              class="p-2 ywicon icon-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 ywicon icon-buzan cursor-pointer hover:text-[#0284ff] !text-[13px] hover:!text-[15px]"
                              @click="unLikeClick(item)"
                           />
                              <template v-if="item.content?.values">
                                 <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
                                       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>
                              </template>
                           </div>
                           <!-- 操作 -->
                           <div
                              v-if="item.role === RoleEnum.assistant && item.content?.values"
                              class="absolute flex items-center right-0 mr-4 mt-2 space-x-2"
                           >
                              <div
                                 class="flex items-center justify-center size-[15px]"
                                 v-if="item.content?.type === AnswerType.Text || item.content?.type === AnswerType.Knowledge"
                              >
                                 <i
                                    class="p-2 ywifont ywicon-copy cursor-pointer hover:text-[#0284ff] hover:!text-[18px]"
                                    @click="copyClick(item)"
                                 />
                              </div>
                              <template v-if="item.content.errCode !== ErrorCode.Message">
                                 <div class="flex items-center justify-center size-[15px]">
                                    <i
                                       :class="{ 'text-[#0284ff]': item.state === AnswerState.Like }"
                                       class="p-2 ywifont ywicon-dianzan cursor-pointer hover:text-[#0284ff] font-medium hover:!text-[18px]"
                                       @click="likeClick(item)"
                                    />
                                 </div>
                                 <div class="flex items-center justify-center size-[15px]">
                                    <i
                                       :class="{ 'text-[#0284ff]': item.state === AnswerState.Unlike }"
                                       class="p-2 ywifont ywicon-buzan cursor-pointer hover:text-[#0284ff] !text-[13px] hover:!text-[15px]"
                                       @click="unLikeClick(item)"
                                    />
                                 </div>
                              </template>
                              <div class="flex items-center justify-center size-[15px] relative">
                                 <i
                                    class="p-2 ywifont ywicon-wentifankui cursor-pointer hover:text-[#0284ff] !text-[13px] hover:!text-[15px]"
                                    @click="
                                       ($event) =>
                                          feedbackClick(
                                             $event,
                                             item,
                                             computedMessageList
                                                .filter((v) => v.role === RoleEnum.assistant)
                                                .findIndex((v) => v.historyId === item.historyId)
                                          )
                                    "
                                 />
                                 <FeedbackPanel
                                    v-show="feedbackIsShow && currentFeedbackMapItem === item"
                                    ref="feedbackPanelRef"
                                    v-model:isShow="feedbackIsShow"
                                    v-model:content="feedbackContent"
                                    :chatItem="currentFeedbackMapItem"
                                    :position="feedbackPosition"
                                 />
                              </div>
                           </div>
                        </div>
                     </div>
                  </div>
                  <Loding v-else class="w-12" :process="process" />
               </div>
            </div>
            <div v-if="showAskMore" class="ml-4 mt-5 text-sm">
               <div class="text-gray-600 mb-5">你可以继续问我:</div>
               <div class="space-y-2 inline-flex flex-col">
                  <div
                     v-for="item in computedMessageList.at(-1).content.askMoreList"
                     :key="item.history_id"
                     class="bg-white p-3 hover:bg-[#c5e0ff] hover:text-[#1c86ff] cursor-pointer rounded-lg"
                     @click="askMoreClick(item)"
                  >
                     {{ item.question }}
               <div v-if="showAskMore" class="ml-4 mt-5 text-sm pb-10">
                  <div class="text-gray-600 mb-5">你可以继续问我:</div>
                  <div class="space-y-2 inline-flex flex-col">
                     <div
                        v-for="item in computedMessageList.at(-1).content.askMoreList"
                        :key="item.history_id"
                        class="bg-white p-3 hover:bg-[#c5e0ff] hover:text-[#1c86ff] cursor-pointer rounded-lg"
                        @click="askMoreClick(item)"
                     >
                        {{ item.question }}
                     </div>
                  </div>
               </div>
            </div>
         </div>
         <div class="sticky bottom-0 w-full p-6 bg-[rgb(247,248,250)] flex justify-center">
            <PlayBar
               v-model:voicePageIsShow="voicePageIsShow"
               :isTalking="isTalking"
               :isHome="false"
               v-model="messageContent.values"
               @sendClick="sendClick"
               @showUpChatClick="showUpChatClick"
               @showDownChatClick="showDownChatClick"
               :style="{ width: chatWidth }"
            ></PlayBar>
         </div>
      </div>
      <div class="sticky bottom-0 w-full p-6 pb-8 bg-[rgb(247,248,250)] flex justify-center">
         <PlayBar
            :isTalking="isTalking"
            v-model="messageContent.values"
            @sendClick="sendChatMessage"
            :style="{ width: chatWidth }"
         ></PlayBar>
      </div>
      <CustomDrawer v-model:isShow="drawerIsShow" @updateChatInput="updateChatInput" />
   </div>
</template>
<script setup lang="ts">
import { ElMessage } from 'element-plus';
import { computed, nextTick, onActivated, onMounted, ref, watch } from 'vue';
import useClipboard from 'vue-clipboard3';
import Loding from './components/Loding.vue';
import type { ChatContent } from './model/types';
import { AnswerType, RoleEnum, answerTypeMapCom, roleImageMap, type ChatMessage, AnswerState } from './model/types';
import { GetHistoryAnswer, QueryHistoryDetail, QuestionAi, SetHistoryAnswerState, getQuestionProcess } from '/@/api/ai/chat';
import PlayBar from '/@/components/chat/components/playBar/PlayBar.vue';
import router from '/@/router';
import { activeChatRoom, activeLLMId, activeRoomId, activeSampleId, activeSectionAId, roomConfig } from '/@/stores/chatRoom';
import { v4 as uuidv4 } from 'uuid';
import _ from 'lodash';
import moment from 'moment';
import { computed, onMounted, ref, triggerRef } from 'vue';
import FeedbackPanel from './components/FeedbackPanel.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, StepItem } from './model/types';
import {
   AnswerState,
   AnswerType,
   RoleEnum,
   answerTypeMapCom,
   roleImageMap,
   type ChatMessage,
   StepEnum,
   stepEnumMap,
} 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,
   activeGroupType,
   activeLLMId,
   activeRoomId,
   activeSampleId,
   activeSectionAId,
   getRoomConfig,
   roomConfig,
} from '/@/stores/chatRoom';
import { deepClone } from '/@/utils/other';
import { ErrorCode } from '/@/utils/request';
const chatWidth = '75%';
const voicePageIsShow = ref(false);
let isTalking = ref(false);
let messageContent = ref<ChatContent>({
   type: AnswerType.Text,
@@ -102,23 +243,17 @@
});
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.role !== RoleEnum.system);
   return messageList.value.filter((v) => !!v);
});
const showAskMore = computed(() => {
   if (!computedMessageList.value || computedMessageList.value.length === 0) return false;
   const last = computedMessageList.value.at(-1);
   const isShow = last?.role === RoleEnum.assistant && last?.content?.values && last.content?.askMoreList?.length > 0;
   return isShow;
});
const parseContent = (res) => {
   if (!res) return null;
   let content: ChatContent = {
      type: AnswerType.Text,
      values: '发生错误!',
      values: '解析失败!',
   };
   switch (res.answer_type) {
@@ -146,7 +281,6 @@
         content = {
            type: AnswerType.Summary,
            values: res.summary,
            error: res.json_msg,
         };
         break;
      case AnswerType.Url:
@@ -155,75 +289,132 @@
            values: res.url,
         };
         break;
      case AnswerType.Map:
         content = {
            type: AnswerType.Map,
            values: res.values,
         };
         break;
      default:
         content = {
            type: AnswerType.Text,
            values: '发生错误!',
            values: '解析失败!',
         };
         break;
   }
   content.askMoreList = _.orderBy(res.context_history, [(item) => Number(item.radio)], ['desc']);
   content.errCode = res?.err_code;
   content.errMsg = res?.json_msg;
   content.origin = res;
   return content;
};
//#region ====================== 查询进度 ======================
let processId = '';
const QUERY_PROCESS_INTERVAL = 1000;
const process = ref('');
let processTimer = null;
let finishProcess = true;
const { clearQueryProcess, process, processId, queryProcess } = useQueryProcess();
const queryProcessApi = async () => {
   const res = await getQuestionProcess({
      process_id: processId,
   }).catch((err) => {
      process.value = err;
   });
//#region ====================== 步骤 step ======================
const activeStep = ref(-1);
const stepList = ref<StepItem[]>([
   {
      title: '意图分析中...',
      status: 0,
   },
   {
      title: '意图分析完成',
      status: 1,
   },
   {
      title: '思考如何执行:指标明细查询',
      status: 1,
   },
   {
      title: '指标明细查询完成',
      status: 1,
   },
]);
   process.value = res.process;
   finishProcess = true;
const resetStep = () => {
   activeStep.value = -1;
   stepList.value = [];
};
const queryProcess = () => {
   processTimer = setInterval(() => {
      if (!finishProcess) return;
      finishProcess = false;
      queryProcessApi();
   }, QUERY_PROCESS_INTERVAL);
};
const clearQueryProcess = () => {
   process.value = '';
   clearInterval(processTimer);
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;
const questionAi = async (text) => {
   if (!currentSectionId) {
      ElMessage.warning('发送失败,未确定应用场景!');
let finalCalcSectionAId = null;
const questionAi = async (text, assistantMsg: ChatMessage) => {
   // processId.value = uuidv4();
   let judgeParams = null;
   if (!preQuestion.value) {
      // const aiContent = computedMessageList.value.filter((item) => item.role === RoleEnum.assistant);
      // const lastQuestion = aiContent[aiContent.length - 2]?.content?.origin?.question;
      // judgeParams = lastQuestion
      //    ? {
      //          prev_question: lastQuestion,
      //      }
      //    : {};
      // 正常回答暂时不采用
      judgeParams = {};
   } else {
      judgeParams = {
         prev_question: preQuestion.value,
      };
   }
   processId = uuidv4();
   let currentSectionAId = '';
   if (activeSectionAId.value) {
      currentSectionAId = activeSectionAId.value;
      activeSectionAId.value = '';
   } else {
      const lastSectionAItem = _.findLast(
         computedMessageList.value as any,
         (item) => item.role === RoleEnum.assistant && !!item.sectionAId
      );
      currentSectionAId = lastSectionAItem?.sectionAId ?? DEFAULT_SECTION_A_ID;
   }
   finalCalcSectionAId = currentSectionAId;
   const params = {
      process_id: processId,
      // process_id: processId.value,
      question: text,
      // FIXME: 暂时这样
      section_a_id: currentSectionId,
      // section_a_id: currentSectionAId,
      history_group_id: currentRouteId,
      raw_mode: roomConfig.value?.[currentRouteId]?.isAnswerByLLM ?? false,
      ...judgeParams,
   } as any;
   if (activeGroupType.value) {
      params.group_type = activeGroupType.value;
   }
   if (currentSampleId) {
      params.sample_id = currentSampleId;
      currentSampleId = '';
   }
   if (currentLLMId) {
      params.llm_id = currentLLMId;
   }
   clearQueryProcess();
   queryProcess();
   const res = await QuestionAi(params).finally(() => {
      clearQueryProcess();
   // if (currentLLMId) {
   //    params.llm_id = currentLLMId;
   // }
   // clearQueryProcess();
   // queryProcess();
   resetStep();
   let res = null;
   await questionStreamByPost(params, (chunkRes) => {
      if (chunkRes.mode === 'result') {
         res = chunkRes.value;
      } else {
         const stepItem = convertProcessItem(chunkRes);
         computedMessageList.value.at(-1).stepList.push(stepItem);
         scrollToBottom();
      }
   }).finally(() => {
      computedMessageList.value.at(-1).stepIsShow = false;
      resetStep();
   });
   questionRes = res;
   const content = parseContent(res);
@@ -236,24 +427,14 @@
      values: '',
   });
const scrollToBottom = () => {
   if (!chatListDom.value) return;
   chatListDom.value.lastElementChild?.scrollIntoView();
};
let currentSectionId = null;
let currentSampleId = null;
let currentSampleId = '';
let currentLLMId = null;
const getAnswerById = async (historyId: string) => {
   return await GetHistoryAnswer({
      history_id: historyId,
   });
};
const sendChatMessage = async (content: ChatContent = messageContent.value) => {
   if (!content?.values) return;
   if (messageList.value.length === 0) {
const sendChatMessage = async (content: ChatContent = messageContent.value, cb?: any, isCallExtParams?: any) => {
   if (!content?.values || isTalking.value || chatListLoading.value) return;
   const isNewChat = messageList.value.length === 0;
   if (isNewChat) {
      if (activeSampleId.value) {
         currentSampleId = activeSampleId.value;
      }
@@ -261,16 +442,19 @@
      if (activeLLMId.value) {
         currentLLMId = activeLLMId.value;
      }
      if (activeSectionAId.value) {
         currentSectionId = activeSectionAId.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 assistantItem: ChatMessage = {
         role: RoleEnum.assistant,
         content: null,
         state: AnswerState.Null,
         stepList: [],
         stepIsShow: true,
      } as any;
      // 发送当前
      messageList.value.push(userItem);
      // 清空输入框
@@ -278,63 +462,73 @@
      // 出现回复,置空出现等待动画
      messageList.value.push(assistantItem);
      // 滚动至当前发送消息
      scrollToBottom();
      let resMsgContent: ChatContent = null;
      resMsgContent = await questionAi(content.values);
      if (isCallExtParams) {
         const extRes = await extCallQuery(isCallExtParams);
         questionRes = extRes;
         resMsgContent = parseContent(extRes);
      } else {
         resMsgContent = await questionAi(content.values, assistantItem);
      }
      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);
   } catch (error: any) {
      appendLastMessageContent({
         type: AnswerType.Text,
         values: '发生错误!',
      });
      // appendLastMessageContent({
      //    type: AnswerType.Text,
      //    values: '发生错误!',
      // });
   } finally {
      isTalking.value = false;
   }
};
const sendClick = (cb) => {
   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 askMoreClick = (item) => {
   if (!item.question) return;
   sendChatMessage({ type: AnswerType.Text, values: item.question });
};
const chatListLoading = ref(false);
const { scrollToBottom } = useScrollToBottom({
   chatListDom: chatListDom,
});
onMounted(async () => {
   const res = await QueryHistoryDetail({
      history_group_id: currentRouteId,
   });
   messageList.value = [];
   // 加载初始数据
   chatListLoading.value = true;
   messageList.value = (res.details ?? []).map((item) => {
      return {
         historyId: item.history_id,
         role: RoleEnum.user,
         content: {
            type: AnswerType.Text,
            values: item.question,
         },
      } as ChatMessage;
   await loadRangeData().finally(() => {
      chatListLoading.value = false;
   });
   currentSectionId = res?.details?.[0]?.section_a_id;
   currentSampleId = res?.details?.[0]?.sample_id;
   const resList = await Promise.all((res.details ?? []).map((item) => getAnswerById(item.history_id)));
   let i = 0;
   resList.map((item, index) => {
      const insertIndex = index + 1 + i;
      messageList.value.splice(insertIndex, 0, {
         historyId: item.answer.history_id,
         role: RoleEnum.assistant,
         content: parseContent(item.answer),
         state: item.answer_state,
      });
      i++;
   });
   if (messageList.value.length === 0) {
      messageContent.value = {
         type: AnswerType.Text,
@@ -342,74 +536,117 @@
      };
      sendChatMessage();
   }
});
let forbidScroll = false;
watch(
   messageList,
   () => {
      if (forbidScroll) return;
      nextTick(() => scrollToBottom());
   },
   {
      deep: true,
   }
);
onActivated(() => {
   if (forbidScroll) return;
   nextTick(() => scrollToBottom());
});
//#region ====================== 聊天内容操作 ======================
const { toClipboard } = useClipboard();
const copyClick = (item) => {
   const type = item.content.type;
   let text = '';
   if (type === AnswerType.Knowledge) {
      text = item.content.values?.map((item) => item.answer).join('\n\n') ?? '';
   } else {
      text = item.content.values;
      setTimeout(() => {
         // 初始状态滚一下
         scrollToBottom();
         setTimeout(() => {
            chatListDom.value.addEventListener('scroll', onChatListScroll);
         }, 300);
      }, 300);
   }
   ElMessage.success('复制成功');
   toClipboard(text);
});
//#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 likeClick = async (item) => {
   const toSetState = item.state === AnswerState.Like ? AnswerState.Null : AnswerState.Like;
   const res = await SetHistoryAnswerState({
      history_id: item.historyId,
      answer_state: toSetState,
   });
   item.state = toSetState;
   forbidScroll = true;
   nextTick(() => {
      forbidScroll = false;
   });
//#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 === null) {
      currentIndex.value = history_data.value.length - 1;
   } 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 === 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,
});
const unLikeClick = async (item) => {
   const toSetState = item.state === AnswerState.Unlike ? AnswerState.Null : AnswerState.Unlike;
   const res = await SetHistoryAnswerState({
      history_id: item.historyId,
      answer_state: toSetState,
   });
   item.state = toSetState;
//#region ====================== 侧边栏drawer ======================
const drawerIsShow = ref(false);
   forbidScroll = true;
   nextTick(() => {
      forbidScroll = false;
   });
const updateChatInput = (content) => {
   messageContent.value.values = content;
};
//#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) {
   height: 20px;
}
</style>