gerson
2025-03-02 0395ae2cf0acc698db8d2cd94e808fae9b0f74bf
src/components/chat/assistant/index.vue
@@ -3,169 +3,158 @@
      <div class="absolute top-0 left-[72px] text-[#8d8e99]">{{ msg?.createTime }}</div>
      <img
         class="rounded-full size-12 flex-0 mr-4"
         class="rounded-full size-12 flex-0"
         :class="{ 'mr-4': msg.role === RoleEnum.assistant }"
         :src="roleImageMap[msg.role]"
         alt=""
         srcset=""
      />
      <div class="flex-auto flex">
         <div class="inline-flex flex-col w-full" >
         <div class="inline-flex flex-col" :class="{ 'w-full': msg.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="msg?.stepList?.length > 0">
                     <!-- #region ====================== 意图分析 ======================-->
                     <div class="flex items-center">
                        <span class="mr-2">意图分析:</span>
                        <div
                           @click="toggleStepList(msg)"
                           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(msg) }}
                           </span>
                           <span class="ywifont" :class="{ 'ywicon-unfold': !msg.stepIsShow, 'ywicon-fold': msg.stepIsShow }"></span>
                        </div>
                     </div>
                     <!-- #endregion -->
                     <!-- #region ====================== 过程输出 ======================-->
                     <el-steps v-show="msg.stepIsShow" class="mt-3" direction="vertical" :active="activeStep">
                        <el-step v-for="(subItem, index) in msg.stepList" :title="subItem.title" :status="stepEnumMap[subItem.status]">
                           <template
                              #icon
                              v-if="index + 1 === msg.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]">
                                 <component
                                    :key="`${msg.historyId}-${index + 1}-${multiChatIndex + 1}`"
                                    v-for="(multiChatItem, multiChatIndex) in subItem.subStep"
                                    :order="`${index + 1}-${multiChatIndex + 1}`"
                                    :item="multiChatItem"
                                    :is="multiChatTypeMapCom[multiChatItem.type]"
                                    :disabled="!(index + 1 === msg.stepList.length && isTalking && msgIndex === computedMessageList.length - 1)"
                                 />
                              </div>
                           </template>
                        </el-step>
                     </el-steps>
                     <!-- #endregion -->
                  </div>
                  <!-- #endregion -->
                  <!-- #region ====================== 用户操作按钮 ======================-->
                  <div
                     v-if="msg.role === RoleEnum.user && msg.content?.values && !isSharePage && !isShareCheck"
                     class="absolute flex items-center bottom-0 group invisible"
                  >
                     <div class="bg-[#fff] flex items-center relative mr-4 space-x-2 flex-nowrap rounded-[6px] py-2 px-2 group-hover:visible">
                        <el-tooltip effect="dark" content="复制" placement="top">
                           <div class="flex items-center justify-center size-[20px]">
                              <i
                                 class="p-2 ywifont ywicon-copy cursor-pointer hover:text-[#0284ff] font-medium !text-[15px] hover:!text-[18px]"
                                 @click="copyUserClick(msg)"
                              />
                           </div>
                        </el-tooltip>
                        <el-tooltip effect="dark" content="设为常用语" placement="top">
                           <div class="flex items-center justify-center size-[20px]">
                              <i
                                 class="p-2 ywifont ywicon-cubelifangti cursor-pointer hover:text-[#0284ff] text-[#000] font-[590] !text-[15px] hover:!text-[18px]"
                                 @click="setCommonQuestionClick(msg)"
                              />
                           </div>
                        </el-tooltip>
                        <el-tooltip effect="dark" content="分享" placement="top">
                           <div class="flex items-center justify-center size-[15px]">
                              <i
                                 :class="{ 'text-[#0284ff]': msg.state === AnswerState.Unlike }"
                                 class="p-2 ywifont ywicon-fenxiang cursor-pointer hover:text-[#0284ff] !text-[15px] hover:!text-[18px]"
                                 @click="shareClick(msg)"
                              />
                           </div>
                        </el-tooltip>
                     </div>
                  </div>
                  <!-- #endregion -->
                  <!-- #region ====================== 消息内容 ======================-->
                  <template v-if="msg.content?.values">
                     <!-- #region ====================== 报错信息 ======================-->
                     <div v-if="msg.content.errCode === ErrorCode.Message" class="flex-column w-full">
                        <p class="text-red-500">
                           {{ msg.content.errMsg }}
                        </p>
                        <div class="mt-3 flex" v-if="showFixQuestion(msg)">
                           <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 msg.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, msg.content.origin)"
                              >
                                 {{ fixItem }}
                  <!-- <template v-if="item.content?.values"> -->
                  <!-- #region ====================== 回答组件 ======================-->
                  <template v-if="msg.content.type === AnswerType.Report">
                     <template v-if="msg?.stepGroup?.length > 0">
                        <div v-for="(num, index) in msg?.stepGroup?.length" :key="index">
                           <!-- 意图分析只展示第一个,后续的 stepGroup 都是空的,用于循环出组件 -->
                           <!-- #region ====================== 意图分析 ======================-->
                           <div class="flex flex-col" v-if="msg?.stepGroup?.[index]?.value?.length > 0 && index === 0">
                              <!-- #region ====================== 意图分析 ======================-->
                              <div class="flex items-center">
                                 <span class="mr-2">意图分析:</span>
                                 <div
                                    @click="toggleStepList(msg?.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(msg?.stepGroup?.[index]) }}
                                    </span>
                                    <span
                                       class="ywifont"
                                       :class="{
                                          'ywicon-unfold': !msg?.stepGroup?.[index].isShow,
                                          'ywicon-fold': msg?.stepGroup?.[index].isShow,
                                       }"
                                    ></span>
                                 </div>
                              </div>
                              <!-- #endregion -->
                              <!-- #region ====================== 过程输出 ======================-->
                              <el-steps v-show="msg?.stepGroup?.[index].isShow" class="mt-3" direction="vertical">
                                 <el-step
                                    :key="`template-${stepIndex}`"
                                    v-for="(subItem, stepIndex) in msg?.stepGroup?.[index].value"
                                    :title="subItem.title"
                                    :status="stepEnumMap[subItem.status]"
                                 >
                                    <template
                                       #icon
                                       v-if="
                                          stepIndex + 1 === msg?.stepGroup?.[index].value.length &&
                                          isTalking &&
                                          isLast &&
                                          (subItem.finishLoading === false || subItem.finishLoading === undefined)
                                       "
                                    >
                                       <span class="ywifont ywicon-loading1 animate-spin !text-[24px]"></span>
                                    </template>
                                    <template #title>
                                       <span class="">
                                          <span v-html="md.render(subItem.title)"></span>
                                          <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="`${msg.historyId}-${stepIndex + 1}-${multiChatIndex + 1}`"
                                             v-for="(multiChatItem, multiChatIndex) in subItem.subStep"
                                          >
                                             <component
                                                :order="`${stepIndex + 1}-${multiChatIndex + 1}`"
                                                :item="multiChatItem"
                                                :is="multiChatTypeMapCom[multiChatItem.type]"
                                                @change="multiChatChange"
                                                :disabled="!(stepIndex + 1 === msg?.stepGroup?.[index].value.length && isTalking && isLast)"
                                             />
                                          </div>
                                       </div>
                                    </template>
                                 </el-step>
                              </el-steps>
                              <!-- #endregion -->
                           </div>
                        </div>
                     </div>
                     <!-- #endregion -->
                     <!-- #region ====================== 回答组件 ======================-->
                     <template v-else>
                        <component
                           :conclusion="msg.conclusion"
                           :is="answerTypeMapCom[msg.content.type]"
                           :data="msg.content.values"
                           :originData="msg"
                           :isTalking="isTalking && msgIndex === computedMessageList.length - 1"
                        />
                        <div
                           v-if="msg.role === RoleEnum.assistant && msg.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 msg.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>
                           <!-- #endregion -->
                           <!-- result 时  recordSetTable 已经加载,summary 时又加载了一次,导致比例列 push 了两次
                              为了解决这个问题,等到 msg.historyId 存在时,再渲染 recordSetTable
                              -->
                           <component
                              v-if="msg.content?.values?.[index]"
                              :reportIndex="index"
                              :conclusion="msg.content.values[index].conclusion"
                              :is="answerTypeMapCom[msg.content.values[index].content.type]"
                              :data="msg.content.values[index].content.values"
                              :originData="msg.content.values[index]"
                              :historyId="msg.historyId"
                              :isTalking="isTalking && isLast"
                           />
                        </div>
                     </template>
                     <!-- #endregion -->
                     <p v-else-if="msg.content?.errCode !== ErrorCode.Message" class="text-info">暂无内容,请重试</p>
                  </template>
                  <component
                     v-else
                     :historyId="msg.historyId"
                     :conclusion="msg.conclusion"
                     :is="answerTypeMapCom[msg.content.type]"
                     :data="msg.content.values"
                     :originData="msg"
                     :isTalking="isTalking && isLast"
                  />
                  <!-- #endregion -->
                  <!-- </template> -->
                  <!-- #region ====================== 报错信息 ======================-->
                  <div v-if="msg.content?.errCode === ErrorCode.Message" class="flex-column w-full">
                     <p class="text-danger">
                        {{ msg.content.errMsg }}
                     </p>
                     <div class="mt-3 flex" v-if="showFixQuestion(msg)">
                        <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 msg.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, msg.content.origin)"
                           >
                              {{ fixItem }}
                           </div>
                        </div>
                     </div>
                  </div>
                  <!-- #endregion -->
                  <!-- #endregion -->
                  <!-- #region ====================== 附加内容 ======================-->
                  <!-- #region ====================== 停止 ======================-->
                  <span v-if="msg.isStopMsg && msg?.role === RoleEnum.assistant" class="text-gray-400 text-[12px]">(已停止)</span>
                  <!-- parseContent 返回为 null -->
                  <p v-if="!msg.content && !isTalking && !msg.isStopMsg" class="text-red-500">暂无数据</p>
                  <p v-if="!msg.content && !isTalking && !msg.isStopMsg && msg.content?.errCode !== ErrorCode.Message" class="text-red-500">
                     暂无数据
                  </p>
                  <!-- #endregion -->
                  <!-- #endregion -->
               </div>
               <!-- #region ====================== ai 消息操作 ======================-->
               <div
                  v-if="msg.role === RoleEnum.assistant && msg.content?.values && !isSharePage && !isShareCheck"
                  class="absolute flex items-center right-0 mr-4 mt-2 space-x-2"
               >
               <div v-if="msg.content?.values && !isSharePage" 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="msg.content?.type === AnswerType.Text || msg.content?.type === AnswerType.Knowledge"
@@ -209,9 +198,7 @@
                                 feedbackClick(
                                    $event,
                                    msg,
                                    computedMessageList
                                       .filter((v) => v.role === RoleEnum.assistant)
                                       .findIndex((v) => v.historyId === msg.historyId)
                                    msgList.filter((v) => v.role === RoleEnum.assistant).findIndex((v) => v.historyId === msg.historyId)
                                 )
                           "
                        />
@@ -227,6 +214,16 @@
                  </el-tooltip>
               </div>
               <!-- #endregion -->
               <div class="absolute flex items-center left-18 mt-2 space-x-2 pb-7">
                  <div
                     v-if="isTalking && isLast"
                     class="text-blue-400 cursor-pointer z-20 py-2 px-2 border border-solid border-blue-400 hover:text-blue-500 hover:border-blue-500 rounded-lg hover:bg-[#ebeffa]"
                     @click="stopGenClick"
                  >
                     停止输出
                  </div>
               </div>
            </div>
         </div>
      </div>
@@ -234,51 +231,130 @@
</template>
<script setup lang="ts" name="AssistantMsg">
import type { CancelTokenSource } from 'axios';
import axios from 'axios';
import { findLast, orderBy } from 'lodash-es';
import moment from 'moment';
import QRCode from 'qrcodejs2-fixes';
import { computed, nextTick, onActivated, onMounted, ref } from 'vue';
import useClipboard from 'vue-clipboard3';
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 type { ChatContent, StepItem } from './model/types';
import { AnswerState, AnswerType, RoleEnum, answerTypeMapCom, roleImageMap, stepEnumMap, type ChatMessage } from './model/types';
import { extCallQuery, questionStreamByPost, shareChatHistoryByPost } from '/@/api/ai/chat';
import PlayBar from '/@/components/chat/components/playBar/PlayBar.vue';
import CustomDrawer from '/@/components/drawer/CustomDrawer.vue';
import { SHARE_URL } from '/@/constants';
import { Logger } from '/@/model/logger/Logger';
import router from '/@/router';
import type { PropType } from 'vue';
import { useAssistantContentOpt } from '../hooks/useAssistantContentOpt';
import type { ChatContent } from '../model/types';
import {
   activeChatRoom,
   activeGroupType,
   activeLLMId,
   activeRoomId,
   activeSampleId,
   activeSectionAId,
   getRoomConfig,
   isSharePage,
   roomConfig,
} from '/@/stores/chatRoom';
import UserMsg from './user/index.vue';
   AnswerState,
   AnswerType,
   MultiChatType,
   RoleEnum,
   answerTypeMapCom,
   roleImageMap,
   stepEnumMap,
   type ChatMessage,
} from '../model/types';
import FeedbackPanel from '../components/FeedbackPanel.vue';
import { multiChatTypeMapCom } from '/@/components/chat/chatComponents/multiChat';
import emitter from '/@/utils/mitt';
import { isSharePage } from '/@/stores/chatRoom';
import { ErrorCode } from '/@/utils/request';
import { toMyFixed } from '/@/utils/util';
import { ElMessage } from 'element-plus';
import { question_stream_reply } from '/@/api/ai/chat';
import { md } from '/@/components/chat/libs/markdown';
const props = defineProps({
    /** @description 当前消息 */
    msg: {
        type: Object,
    },
})
   /** @description 当前消息 */
   msg: {
      type: Object as PropType<ChatMessage>,
   },
   msgList: {
      type: Array as PropType<Array<ChatMessage>>,
   },
   isLast: {
      type: Boolean,
      default: false,
   },
   isTalking: {
      type: Boolean,
      default: false,
   },
});
const emit = defineEmits({
   sendChatMessage: (content: ChatContent) => true,
   shareMsg: (msg: ChatMessage) => true,
   stopGenClick: () => true,
});
const sendChatMessage = (content: ChatContent) => {
   emit('sendChatMessage', content);
};
const stopGenClick = () => {
   emit('stopGenClick');
};
//#region ====================== 步骤 step ======================
const toggleStepLabel = (item: any) => (item.isShow ? '收起' : '展开');
const toggleStepList = (item: any) => {
   item.isShow = !item.isShow;
};
//#endregion
const {
   copyClick,
   likeClick,
   unLikeClick,
   feedbackPosition,
   feedbackIsShow,
   feedbackContent,
   feedbackPanelRef,
   currentFeedbackMapItem,
   feedbackClick,
   fixQuestionClick,
   showFixQuestion,
} = useAssistantContentOpt({
   sendChatMessage,
});
//#region ====================== 分享 ======================
const shareClick = async (item: ChatMessage) => {
   emit('shareMsg', item);
};
//#endregion
const multiChatChange = async (replyId: string, val: any) => {
   const res = await question_stream_reply({
      start_time: val.start_time,
      end_time: val.end_time,
      reply_id: replyId,
   });
};
</script>
<style scoped lang="scss"></style>
<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>