| | |
| | | <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" |
| | | v-for="(item, msgIndex) of computedMessageList" |
| | | :key="`${item.historyId}_${item.role}`" |
| | | > |
| | | <div class="absolute top-0 left-[72px] text-[#8d8e99]">{{ item?.createTime }}</div> |
| | |
| | | :style="{ backgroundColor: item.role === RoleEnum.user ? 'rgb(197 224 255)' : 'white' }" |
| | | > |
| | | <div class="flex flex-col" v-if="item?.stepList?.length > 0"> |
| | | <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 mb-3 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 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> |
| | | |
| | | <!-- 过程输出 --> |
| | |
| | | :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 |
| | | #icon |
| | | v-if="index + 1 === item.stepList.length && isTalking && msgIndex === computedMessageList.length - 1" |
| | | > |
| | | <span class="ywifont ywicon-loading1 animate-spin !text-[24px]"></span> |
| | | </template> |
| | | <template #title> |
| | | <span class="text-sm">{{ subItem.title }}</span> |
| | | <span class="text-sm" |
| | | >{{ subItem.title }}<span v-if="subItem.ms" class="text-green-600">{{ `(${subItem.ms})` }}</span></span |
| | | > |
| | | </template> |
| | | </el-step> |
| | | </el-steps> |
| | |
| | | </div> |
| | | </div> |
| | | <template v-else> |
| | | <el-popover placement="bottom-start" trigger="hover" :popper-style="{ minWidth: '70px' }" :width="70"> |
| | | <template #default> |
| | | <div class="action" v-if="item.role === RoleEnum.user"> |
| | | <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]" |
| | | @click="copyUserClick(item)" |
| | | /> |
| | | </div> |
| | | <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]" |
| | | @click="setCommonQuestionClick(item)" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <template #reference> |
| | | <component |
| | | :is="answerTypeMapCom[item.content.type]" |
| | | v-if="item.role === RoleEnum.user" |
| | | :data="item.content.values" |
| | | :originData="item" |
| | | /> |
| | | </template> |
| | | </el-popover> |
| | | <component |
| | | :conclusion="item.conclusion" |
| | | :is="answerTypeMapCom[item.content.type]" |
| | | v-if="item.role !== RoleEnum.user" |
| | | :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" |
| | |
| | | </div> |
| | | <!-- 操作 --> |
| | | <div |
| | | v-if="item.role === RoleEnum.user && item.content?.values" |
| | | class="absolute flex items-center right-0 mr-4 space-x-2" |
| | | > |
| | | <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(item)" |
| | | /> |
| | | </div> |
| | | <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(item)" |
| | | /> |
| | | </div> |
| | | </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> |
| | | <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> |
| | | <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> |
| | | |
| | | <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 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> |
| | | </div> |
| | | </div> |
| | |
| | | @showUpChatClick="showUpChatClick" |
| | | @showDownChatClick="showDownChatClick" |
| | | :style="{ width: chatWidth }" |
| | | :setCommonQuestionInfo="setCommonQuestionInfo" |
| | | ></PlayBar> |
| | | </div> |
| | | </div> |
| | |
| | | <script setup lang="ts"> |
| | | import _ from 'lodash'; |
| | | import moment from 'moment'; |
| | | import { v4 as uuidv4 } from 'uuid'; |
| | | import { computed, onMounted, ref } from 'vue'; |
| | | import FeedbackPanel from './components/FeedbackPanel.vue'; |
| | | import Loding from './components/Loding.vue'; |
| | | import { useAssistantContentOpt } from './hooks/useAssistantContentOpt'; |
| | | import { useQueryProcess } from './hooks/useQueryProcess'; |
| | | import { useScrollLoad } from './hooks/useScrollLoad'; |
| | | import { convertProcessItem, useScrollLoad } from './hooks/useScrollLoad'; |
| | | import { useScrollToBottom } from './hooks/useScrollToBottom'; |
| | | import type { ChatContent } from './model/types'; |
| | | import { AnswerState, AnswerType, RoleEnum, answerTypeMapCom, roleImageMap, type ChatMessage } from './model/types'; |
| | | import { QuestionAi, extCallQuery, questionStreamByPost } from '/@/api/ai/chat'; |
| | | import type { ChatContent, StepItem } from './model/types'; |
| | | import { AnswerState, AnswerType, RoleEnum, answerTypeMapCom, roleImageMap, stepEnumMap, type ChatMessage } from './model/types'; |
| | | import { extCallQuery, questionStreamByPost } from '/@/api/ai/chat'; |
| | | import PlayBar from '/@/components/chat/components/playBar/PlayBar.vue'; |
| | | import CustomDrawer from '/@/components/drawer/CustomDrawer.vue'; |
| | | import { Logger } from '/@/model/logger/Logger'; |
| | | import router from '/@/router'; |
| | | import { |
| | | activeChatRoom, |
| | |
| | | roomConfig, |
| | | } from '/@/stores/chatRoom'; |
| | | import { ErrorCode } from '/@/utils/request'; |
| | | import { ElMessage } from 'element-plus'; |
| | | import useClipboard from 'vue-clipboard3'; |
| | | import { toMyFixed, toPercent } from '/@/utils/util'; |
| | | const chatWidth = '75%'; |
| | | const voicePageIsShow = ref(false); |
| | | let isTalking = ref(false); |
| | |
| | | const computedMessageList = computed(() => { |
| | | return messageList.value.filter((v) => !!v); |
| | | }); |
| | | const parseContent = (res) => { |
| | | const parseContent = (res, reportIsShow = false) => { |
| | | if (!res) return null; |
| | | let content: ChatContent = { |
| | | type: AnswerType.Text, |
| | |
| | | case AnswerType.Summary: |
| | | content = { |
| | | type: AnswerType.Summary, |
| | | values: res.summary, |
| | | values: res.summary?.map((item) => { |
| | | item.reportIsShow = reportIsShow; |
| | | return item; |
| | | }), |
| | | }; |
| | | break; |
| | | case AnswerType.Url: |
| | |
| | | let questionRes = null; |
| | | |
| | | let finalCalcSectionAId = null; |
| | | const questionAi = async (text, assistantMsg: ChatMessage) => { |
| | | // processId.value = uuidv4(); |
| | | const questionAi = async (text) => { |
| | | let judgeParams = null; |
| | | if (!preQuestion.value) { |
| | | // const aiContent = computedMessageList.value.filter((item) => item.role === RoleEnum.assistant); |
| | |
| | | // queryProcess(); |
| | | resetStep(); |
| | | let res = null; |
| | | await questionStreamByPost(params, (chunkRes) => { |
| | | if (chunkRes.mode === 'result') { |
| | | res = chunkRes.value; |
| | | } else { |
| | | let lastTimestamp = new Date().getTime(); |
| | | const resultP = new Promise(async (resolve, reject) => { |
| | | await questionStreamByPost(params, (chunkRes) => { |
| | | Logger.info('chunk response:\n\n' + JSON.stringify(chunkRes)); |
| | | if (chunkRes.mode === 'result') { |
| | | res = chunkRes.value; |
| | | resolve(res); |
| | | chunkRes.value = '准备数据分析'; |
| | | } |
| | | |
| | | if (chunkRes.mode === 'conclusion') { |
| | | computedMessageList.value.at(-1).conclusion = chunkRes.value; |
| | | chunkRes.value = '分析结束'; |
| | | } |
| | | const stepList = computedMessageList.value.at(-1).stepList; |
| | | const currentTimeStamp = new Date().getTime(); |
| | | const ms = toMyFixed(currentTimeStamp - lastTimestamp, 2) + ' ms'; |
| | | if (chunkRes.mode === 'finish') { |
| | | stepList.at(-1).ms = ms; |
| | | |
| | | isTalking.value = false; |
| | | return; |
| | | } |
| | | |
| | | if (stepList?.length >= 1) { |
| | | stepList.at(-1).ms = ms; |
| | | } |
| | | lastTimestamp = currentTimeStamp; |
| | | |
| | | const stepItem = convertProcessItem(chunkRes); |
| | | computedMessageList.value.at(-1).stepList.push(stepItem); |
| | | stepList.push(stepItem); |
| | | scrollToBottom(); |
| | | } |
| | | }).finally(() => { |
| | | computedMessageList.value.at(-1).stepIsShow = false; |
| | | resetStep(); |
| | | }) |
| | | .catch((err) => { |
| | | throw err; |
| | | }) |
| | | .finally(() => { |
| | | isTalking.value = false; |
| | | |
| | | computedMessageList.value.at(-1).stepIsShow = false; |
| | | resetStep(); |
| | | }); |
| | | }); |
| | | questionRes = res; |
| | | const content = parseContent(res); |
| | | |
| | | questionRes = await resultP; |
| | | const content = parseContent(res, true); |
| | | return content; |
| | | }; |
| | | |
| | |
| | | if (isCallExtParams) { |
| | | const extRes = await extCallQuery(isCallExtParams); |
| | | questionRes = extRes; |
| | | resMsgContent = parseContent(extRes); |
| | | resMsgContent = parseContent(extRes, true); |
| | | } else { |
| | | resMsgContent = await questionAi(content.values, assistantItem); |
| | | resMsgContent = await questionAi(content.values); |
| | | } |
| | | nextUserMsgEndIndex.value++; |
| | | if (isNewChat) { |
| | |
| | | // type: AnswerType.Text, |
| | | // values: '发生错误!', |
| | | // }); |
| | | } finally { |
| | | isTalking.value = false; |
| | | } |
| | | }; |
| | | |
| | |
| | | //显示上一条消息 |
| | | const showUpChatClick = () => { |
| | | if (computedMessageList.value.length === 0) return; |
| | | if (currentIndex.value === null) { |
| | | currentIndex.value = history_data.value.length - 1; |
| | | 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; |
| | | } |
| | |
| | | //显示下一条消息 |
| | | 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 { |
| | |
| | | }; |
| | | //#endregion |
| | | //#region ====================== 用户询问的问题设置为常用语 ====================== |
| | | const setCommonQuestionInfo = ref({}); |
| | | const { toClipboard } = useClipboard(); |
| | | |
| | | //用户复制问题 |
| | | const copyUserClick = () => {}; |
| | | const copyUserClick = (item) => { |
| | | const text = item.content.values; |
| | | ElMessage.success('复制成功'); |
| | | toClipboard(text); |
| | | }; |
| | | //用户问题设置为常用语 |
| | | const setCommonQuestionClick = () => {}; |
| | | const setCommonQuestionClick = (item) => { |
| | | setCommonQuestionInfo.value = item; |
| | | }; |
| | | //#endregion |
| | | </script> |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | .el-step.is-horizontal.stepActive { |
| | | .el-step__head.is-finish { |
| | | .el-step__line { |
| | | // 当前步骤数横线样式设置 |
| | | .el-step__line-inner { |
| | | width: 50% !important; |
| | | border-width: 1px !important; |
| | | } |
| | | } |
| | | |
| | | // 当前步骤数圆圈样式设置 |
| | | .el-step__icon.is-text { |
| | | // background: #409eff; |
| | | color: #fff; |
| | | } |
| | | } |
| | | :deep(.el-step__icon.is-text) { |
| | | --radius-size: 24px; |
| | | width: var(--radius-size); |
| | | height: var((--radius-size)); |
| | | } |
| | | |
| | | :deep(.el-step__icon-inner) { |