From 3e52ec41322cd3bbb96f8d1492dc9b69cebd6661 Mon Sep 17 00:00:00 2001 From: wujingjing <gersonwu@qq.com> Date: 星期一, 04 十一月 2024 15:34:50 +0800 Subject: [PATCH] 字体 ,超时 --- src/components/chat/Chat.vue | 401 ++++++++++++++++++++++++++++++++++++++------------------ 1 files changed, 272 insertions(+), 129 deletions(-) diff --git a/src/components/chat/Chat.vue b/src/components/chat/Chat.vue index 010fb3f..6827430 100644 --- a/src/components/chat/Chat.vue +++ b/src/components/chat/Chat.vue @@ -1,14 +1,19 @@ <template> <div class="flex h-full"> <div class="flex flex-col h-full flex-auto"> - <div class="h-full flex flex-col items-center overflow-y-auto"> - <div ref="chatListDom" class="h-full" :style="{ width: chatWidth }"> + <div 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="index" + :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 }" @@ -18,55 +23,89 @@ /> <div class="flex-auto flex" :class="{ 'justify-end': item.role === RoleEnum.user }"> <div class="inline-flex flex-col" :class="{ 'w-full': item.role === RoleEnum.assistant }"> - <div class="w-full" v-if="item.content?.values"> + <div class="w-full"> <div class="text-sm rounded-[6px] p-4 leading-relaxed" :style="{ backgroundColor: item.role === RoleEnum.user ? 'rgb(197 224 255)' : 'white' }" > - <div v-if="item.content.errCode === ErrorCode.Message" class="flex-column w-full"> - <p class="text-red-500"> - {{ item.content.errMsg }} - </p> - <div class="mt-5 flex items-center" v-if="showFixQuestion(item)"> - <div class="text-gray-600 flex-0"> - {{ item.content.origin.err_json.fix_question.title + '锛�' }} - </div> - <div class="ml-1 space-x-2 inline-flex flex-wrap"> - <div - v-for="fixItem in item.content.origin.err_json.fix_question?.values" - :key="fixItem" - class="bg-gray-200 p-3 hover:bg-[#c5e0ff] hover:text-[#1c86ff] cursor-pointer rounded-lg" - @click="fixQuestionClick(fixItem, item.content.origin)" - > - {{ fixItem.title }} - </div> - </div> - </div> - </div> - <template v-else> - <component :is="answerTypeMapCom[item.content.type]" :data="item.content.values" :originData="item" /> - + <div class="flex flex-col" v-if="item?.stepList?.length > 0"> <div - v-if="item.role === RoleEnum.assistant && item.content.origin?.ext_call_list" - class="flex font-bold items-center mt-6" + @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" > - <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 }} + <span > + {{ toggleStepLabel(item) }} + </span> + <span class="ywifont" :class="{ 'ywicon-unfold': !item.stepIsShow, 'ywicon-fold': item.stepIsShow }"></span> + </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> + + <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" class="absolute flex items-center right-0 mr-4 mt-2 space-x-2"> + <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" @@ -118,12 +157,10 @@ </div> </div> </div> - - <Loding v-if="isTalking && index === computedMessageList.length - 1" class="w-fit" :process="process" /> </div> </div> </div> - <div v-if="showAskMore" class="ml-4 mt-5 text-sm"> + <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 @@ -139,13 +176,15 @@ </div> </div> - <div class="sticky bottom-0 w-full p-6 pb-8 bg-[rgb(247,248,250)] flex justify-center"> + <div class="sticky bottom-0 w-full 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> @@ -156,22 +195,40 @@ </template> <script setup lang="ts"> -import { ElMessage } from 'element-plus'; import _ from 'lodash'; -import { v4 as uuidv4 } from 'uuid'; -import { computed, onMounted, ref } from 'vue'; +import moment from 'moment'; +import { computed, onMounted, ref, triggerRef } 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 { convertProcessItem, useScrollLoad } from './hooks/useScrollLoad'; import { useScrollToBottom } from './hooks/useScrollToBottom'; -import type { ChatContent } from './model/types'; -import { AnswerState, AnswerType, RoleEnum, answerTypeMapCom, roleImageMap, type ChatMessage } from './model/types'; -import { GetHistoryAnswer, QueryHistoryDetail, QuestionAi, extCallQuery } from '/@/api/ai/chat'; +import type { ChatContent, StepItem } from './model/types'; +import { + AnswerState, + AnswerType, + 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, activeLLMId, activeSampleId, activeSectionAId, getRoomConfig, roomConfig } from '/@/stores/chatRoom'; +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%'; @@ -183,12 +240,12 @@ }); const currentRoute = router.currentRoute; const currentRouteId = currentRoute.value.query.id as string; +activeRoomId.value = currentRouteId; const chatListDom = ref<HTMLDivElement>(); const messageList = ref<ChatMessage[]>([]); const computedMessageList = computed(() => { return messageList.value.filter((v) => !!v); }); - const parseContent = (res) => { if (!res) return null; let content: ChatContent = { @@ -248,15 +305,47 @@ content.origin = res; return content; }; - const { clearQueryProcess, process, processId, queryProcess } = useQueryProcess(); -const DEFAULT_SECTION_A_ID = 'knowledge_base'; +//#region ====================== 姝ラ step ====================== +const activeStep = ref(-1); +const stepList = ref<StepItem[]>([ + { + title: '鎰忓浘鍒嗘瀽涓�...', + status: 0, + }, + { + title: '鎰忓浘鍒嗘瀽瀹屾垚', + status: 1, + }, + { + title: '鎬濊�冨浣曟墽琛�:鎸囨爣鏄庣粏鏌ヨ', + status: 1, + }, + { + title: '鎸囨爣鏄庣粏鏌ヨ瀹屾垚', + status: 1, + }, +]); + +const resetStep = () => { + activeStep.value = -1; + stepList.value = []; +}; + +const toggleStepLabel = (item: ChatMessage) => (item.stepIsShow ? '鏀惰捣' : '灞曞紑'); +const toggleStepList = (item: ChatMessage) => { + item.stepIsShow = !item.stepIsShow; +}; + +//#endregion + +const DEFAULT_SECTION_A_ID = 'knowledge_base'; let questionRes = null; let finalCalcSectionAId = null; -const questionAi = async (text) => { - processId.value = uuidv4(); +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); @@ -287,14 +376,18 @@ finalCalcSectionAId = currentSectionAId; const params = { - process_id: processId.value, + // process_id: processId.value, question: text, // FIXME: 鏆傛椂杩欐牱 - section_a_id: currentSectionAId, + // section_a_id: currentSectionAId, history_group_id: currentRouteId, raw_mode: roomConfig.value?.[currentRouteId]?.isAnswerByLLM ?? false, ...judgeParams, } as any; + + if (activeGroupType.value) { + params.group_type = activeGroupType.value; + } if (currentSampleId) { params.sample_id = currentSampleId; @@ -304,10 +397,21 @@ // if (currentLLMId) { // params.llm_id = currentLLMId; // } - clearQueryProcess(); - queryProcess(); - const res = await QuestionAi(params).finally(() => { - clearQueryProcess(); + // 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); @@ -324,14 +428,8 @@ let currentLLMId = null; -const getAnswerById = async (historyId: string) => { - return await GetHistoryAnswer({ - history_id: historyId, - }); -}; - const sendChatMessage = async (content: ChatContent = messageContent.value, cb?: any, isCallExtParams?: any) => { - if (!content?.values || isTalking.value) return; + if (!content?.values || isTalking.value || chatListLoading.value) return; const isNewChat = messageList.value.length === 0; if (isNewChat) { if (activeSampleId.value) { @@ -347,7 +445,13 @@ 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); // 娓呯┖杈撳叆妗� @@ -355,14 +459,17 @@ // 鍑虹幇鍥炲锛岀疆绌哄嚭鐜扮瓑寰呭姩鐢� messageList.value.push(assistantItem); + // 婊氬姩鑷冲綋鍓嶅彂閫佹秷鎭� + scrollToBottom(); + if (isCallExtParams) { const extRes = await extCallQuery(isCallExtParams); questionRes = extRes; resMsgContent = parseContent(extRes); } else { - resMsgContent = await questionAi(content.values); + resMsgContent = await questionAi(content.values, assistantItem); } - + nextUserMsgEndIndex.value++; if (isNewChat) { const firstResCb = getRoomConfig(currentRouteId, 'firstResCb'); firstResCb?.(resMsgContent); @@ -374,6 +481,10 @@ assistantItem.historyId = questionRes.history_id; assistantItem.sectionAId = finalCalcSectionAId; appendLastMessageContent(resMsgContent); + setTimeout(() => { + // 鏀跺埌鍥炲锛岀户缁粴 + scrollToBottom(); + }, 300); } catch (error: any) { // appendLastMessageContent({ // type: AnswerType.Text, @@ -388,64 +499,33 @@ sendChatMessage(messageContent.value, cb); }; const appendLastMessageContent = (content: ChatContent) => { + const currentTime = moment().format('MM鏈圖D鏃� 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 LOAD_CHAT_LIMIT = 10; +const chatListLoading = ref(false); -// 鎵�鏈夌敤鎴锋彁闂巻鍙茶褰� -let userMsgHistory = []; -// 涓嬫鍔犺浇鐢ㄦ埛鎻愰棶绱㈠紩浣嶇疆 -let nextUserMsgEndIndex = 0; +const { scrollToBottom } = useScrollToBottom({ + chatListDom: chatListDom, +}); onMounted(async () => { - const res = await QueryHistoryDetail({ - history_group_id: currentRouteId, - }); - userMsgHistory = res.details ?? []; - nextUserMsgEndIndex = userMsgHistory.length; - // 鎴彇鍊掓暟 LOAD_CHAT_LIMIT 鏉$敤鎴锋秷鎭� - const currentUserMsgList = userMsgHistory.slice(nextUserMsgEndIndex - LOAD_CHAT_LIMIT, nextUserMsgEndIndex); - messageList.value = currentUserMsgList.map((item) => { - return { - historyId: item.history_id, - role: RoleEnum.user, - content: { - type: AnswerType.Text, - values: item.question, - }, - } as ChatMessage; - }); - const sectionAIdMap = new Map(); + messageList.value = []; + // 鍔犺浇鍒濆鏁版嵁 + chatListLoading.value = true; - // 鑾峰彇缁撴灉鎻掑叆鍒扮敤鎴锋彁闂箣鍚� - currentUserMsgList.map((item) => { - sectionAIdMap.set(item.history_id, item.section_a_id); - getAnswerById(item.history_id).then((aiRobot) => { - // 鐢ㄦ埛鎻愰棶绱㈠紩 - const userMsgIndex = messageList.value.findIndex((subItem) => subItem.historyId === item.history_id); - const userMsg = messageList.value[userMsgIndex]; - // values 鍙栧洖绛斾箣鍚庢渶缁堢殑 question - userMsg.content.values = aiRobot.answer?.question ?? userMsg.content.values; - messageList.value.splice( - userMsgIndex + 1, - 0, - aiRobot.answer === null - ? null - : { - historyId: aiRobot.answer?.history_id, - role: RoleEnum.assistant, - content: parseContent(aiRobot.answer), - state: aiRobot.answer_state, - sectionAId: sectionAIdMap.get(aiRobot.answer.history_id), - } - ); - }); + await loadRangeData().finally(() => { + chatListLoading.value = false; }); - if (messageList.value.length === 0) { messageContent.value = { type: AnswerType.Text, @@ -453,14 +533,17 @@ }; sendChatMessage(); + } else { + setTimeout(() => { + // 鍒濆鐘舵�佹粴涓�涓� + scrollToBottom(); + + setTimeout(() => { + chatListDom.value.addEventListener('scroll', onChatListScroll); + }, 300); + }, 300); } }); - -const { forbidScroll } = useScrollToBottom({ - chatListDom: chatListDom, - displayMessageList: computedMessageList, -}); - //#region ====================== 鍏宠仈鏌ヨ ====================== const relativeQueryClick = async (val) => { sendChatMessage( @@ -478,7 +561,32 @@ ); }; //#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, @@ -495,7 +603,6 @@ showFixQuestion, showAskMore, } = useAssistantContentOpt({ - forbidScroll, sendChatMessage, displayMessageList: computedMessageList, }); @@ -509,10 +616,46 @@ //#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); + } + } +} + +.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-inner) { + font-size: 16px !important; +} +:deep(.el-step__description) { + height: 20px; +} </style> -- Gitblit v1.9.3