From 28706df7da34b8854cdce96ad89c035eaded6ea9 Mon Sep 17 00:00:00 2001 From: wujingjing <gersonwu@qq.com> Date: 星期三, 09 四月 2025 15:54:50 +0800 Subject: [PATCH] 完善数字人 --- src/components/chat/messageList/index.vue | 550 +++--------------------------------------------------- 1 files changed, 38 insertions(+), 512 deletions(-) diff --git a/src/components/chat/messageList/index.vue b/src/components/chat/messageList/index.vue index 1491d1d..6029e7b 100644 --- a/src/components/chat/messageList/index.vue +++ b/src/components/chat/messageList/index.vue @@ -1,6 +1,6 @@ <template> - <div class="message-list"> - <div v-for="(item, msgIndex) of msgList" :key="`${item.historyId}_${item.role}`"> + <div class="message-list pb-10"> + <div v-for="(item, msgIndex) of msgList" :key="`${item.historyId ?? msgIndex}_${item.role}`"> <UserMsg :msg="item" @shareClick="shareClick" @@ -15,10 +15,11 @@ :isLast="msgIndex === msgList.length - 1" @sendChatMessage="sendChatMessage" @shareMsg="shareClick" + @stopGenClick="stopGenClick" :isTalking="isTalking" /> </div> - <div v-if="showAskMore" class="ml-4 mt-5 pb-10"> + <div v-if="showAskMore" class="ml-4 mt-5 "> <div class="text-gray-600 mb-5">浣犲彲浠ョ户缁棶鎴戯細</div> <div class="space-y-2 inline-flex flex-col"> <div @@ -35,468 +36,30 @@ </template> <script setup lang="ts" name="MessageList"> -import type { CancelTokenSource } from 'axios'; -import axios from 'axios'; -import { orderBy } from 'lodash-es'; -import moment from 'moment'; -import { computed, ref } from 'vue'; -import { convertProcessItem, formatShowTimeYear, useScrollLoad } from '../hooks/useScrollLoad'; -import type { ChatContent } from '../model/types'; -import { AnswerState, AnswerType, RoleEnum, type ChatMessage } from '../model/types'; -import { questionStreamByPost } from '/@/api/ai/chat'; -import { Logger } from '/@/model/logger/Logger'; - -import { ElMessage } from 'element-plus'; +import { computed, defineProps, type PropType } from 'vue'; import AssistantMsg from '../assistant/index.vue'; -import ChatContainer from '../components/ChatContainer.vue'; -import ShareLinkDlg from '../components/shareLink/index.vue'; +import type { ChatContent, ChatMessage, ContextHistory } from '../model/types'; +import { RoleEnum } from '../model/types'; import UserMsg from '../user/index.vue'; -import router from '/@/router'; -import { activeGroupType, activeLLMId, activeRoomId, activeSampleId, isSharePage, roomConfig } from '/@/stores/chatRoom'; -import { useCompRef } from '/@/utils/types'; -import { toMyFixed } from '/@/utils/util'; -const props = defineProps<{ - msgList: Array<ChatMessage>; -}>(); -const containerRef = useCompRef(ChatContainer); -const chatListDom = computed(() => containerRef.value?.chatListDom); +import { isSharePage } from '/@/stores/chatRoom'; -let isTalking = ref(false); - -let messageContent = ref<ChatContent>({ - type: AnswerType.Text, - values: '', -}); -const currentRoute = router.currentRoute; -const currentRouteId = currentRoute.value.query.id as string; -activeRoomId.value = currentRouteId; -const messageList = ref<ChatMessage[]>([]); -const preQuestion = ref(null); - -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: '瑙f瀽澶辫触锛�', - }; - 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: '瑙f瀽澶辫触锛�', - }; - break; - } - if (!extraContent) { - content = { - ...content, - ...curExtraContent, - }; - } else { - content = { - ...content, - ...extraContent, - }; - } - - return content; -}; - -let questionRes = null; -let position = null; - -let lastAxiosSource: CancelTokenSource = null; -const questionAi = async (text) => { - let judgeParams = null; - if (!preQuestion.value) { - judgeParams = {}; - } else { - judgeParams = { - prev_question: preQuestion.value, - }; - } - - const params = { - question: text, - history_group_id: currentRouteId, - 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(','); - } - - if (activeGroupType.value) { - params.group_type = activeGroupType.value; - } - - if (currentSampleId) { - params.sample_id = currentSampleId; - currentSampleId = ''; - } - - let lastTimestamp = new Date().getTime(); - questionRes = {}; - let lastIsResult = false; - const resultP = new Promise((resolve, reject) => { - const currentSource = axios.CancelToken.source(); - lastAxiosSource = currentSource; - - const getResReport = () => { - const resReport = { - answer_type: AnswerType.Report, - reports: [], - }; - return resReport; - }; - const checkReportEmpty = () => { - const isEmpty = !questionRes?.reports || questionRes?.reports?.length === 0; - - return isEmpty; - }; - questionStreamByPost( - params, - (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; - resolve(resReport); - } else { - const lastMsg = props.msgList.at(-1); - - // 宸茬粡瑙f瀽杩囦竴娆� reports - lastMsg.content.values.push({ - content: parseContent(res, true, { - origin: res, - }), - }); - } - return; - // chunkRes.value = '鍑嗗鏁版嵁鍒嗘瀽'; - } - - if (chunkRes.mode === 'summary') { - const lastMsg = props.msgList.at(-1); - const extraContent = parseExtraContent(chunkRes.value); - const isReportEmpty = checkReportEmpty(); - // 娌℃湁缁忚繃 result 鎶ュ憡杩樻病鍒濆鍖� - if (isReportEmpty) { - const resReport = getResReport(); - questionRes = resReport; - } - // 姝ゅ璇濆凡缁忓姞鍏ュ埌瀵硅瘽鍒楄〃 - if (lastMsg.content?.values && extraContent) { - for (const key in extraContent) { - if (Object.prototype.hasOwnProperty.call(extraContent, key)) { - const value = extraContent[key]; - if (!lastMsg.content[key] || (Array.isArray(lastMsg.content[key]) && lastMsg.content[key].length === 0)) { - lastMsg.content[key] = value; - } - } - } - - lastMsg.historyId = chunkRes.value.history_id; - const userMsg = props.msgList.at(-2); - userMsg.historyId = chunkRes.value.history_id; - userMsg.content.values = chunkRes.value.question; - } - - if (Object.keys(questionRes).length === 0) { - questionRes = chunkRes.value; - } - - // 姝ゅ璇濊繕鏈姞鍏ュ埌瀵硅瘽鍒楄〃 - if (!lastMsg.content?.values && questionRes) { - questionRes = { - ...questionRes, - ...chunkRes.value, - }; - } - - if (isReportEmpty) { - resolve(questionRes); - } - // props.msgList[props.msgList.length - 1] = finalMsg; - scrollToBottom(); - // chunkRes.value = '浣犲彲浠ョ户缁棶鎴�'; - return; - } - - if (chunkRes.mode === 'conclusion') { - const lastReport = props.msgList.at(-1)?.content?.values?.at(-1); - if (lastReport) { - lastReport.conclusion = chunkRes.value; - chunkRes.value = '鍒嗘瀽缁撴潫'; - } - } - - if (chunkRes.mode === 'question') { - const lastGroup = props.msgList.at(-1).stepGroup.at(-1); - const stepList = lastGroup?.value ?? []; - const lastStepItem = stepList.at(-1); - if (!lastStepItem.subStep) { - lastStepItem.subStep = []; - } - lastStepItem.subStep.push({ - type: chunkRes.value.type, - data: chunkRes.value, - }); - scrollToBottom(); - return; - } - // 鏆傛椂涓嶈�冭檻澶氫釜 report鎯呭喌 - - // if (lastIsResult && chunkRes.mode !== 'finish') { - // // 寮�濮嬪鍔犳柊鐨� stepGroup - // props.msgList.at(-1).stepGroup.push({ - // value: [], - // isShow: true, - // }); - // lastIsResult = false; - // } - const lastGroup = props.msgList.at(-1).stepGroup.at(-1); - const stepList = lastGroup?.value ?? []; - 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; - } else { - const stepGroup = props.msgList.at(-1).stepGroup; - if (stepGroup.length > 1) { - const lastStepList = stepGroup.at(-2).value; - lastStepList.at(-1).ms = ms; - } - } - lastTimestamp = currentTimeStamp; - const stepItem = convertProcessItem(chunkRes); - - stepList.push(stepItem); - // 寮哄埗瑙﹀彂鏇存柊 - - scrollToBottom(); - }, - { - cancelToken: currentSource.token, - } - ) - .catch((err) => { - throw err; - }) - .finally(() => { - isTalking.value = false; - // 鏀惰捣鎵�鏈� stepGroup - props.msgList.at(-1).stepGroup.forEach((item) => { - item.isShow = false; - }); - }); - }); - - await resultP; - const content = parseContent(questionRes, true); - return content; -}; - -const clearMessageContent = () => - (messageContent.value = { - type: AnswerType.Text, - values: '', - }); - -let currentSampleId = ''; - -let currentLLMId = null; - -const stopGenClick = () => { - lastAxiosSource?.cancel(); - isTalking.value = false; - chatListLoading.value = false; - props.msgList.at(-1).isStopMsg = true; -}; - -const checkCanSend = (content: ChatContent = messageContent.value) => { - if (!content?.values) { - return false; - } - if (isTalking.value || chatListLoading.value) { - ElMessage.warning('ai 姝e湪鍥炲涓紝璇风◢鍚庡皾璇曟彁闂�'); - return false; - } - return true; -}; - -const addChatItem = (content: ChatContent) => { - 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(); - return [userItem, assistantItem]; -}; - -const sendChatMessage = async (content: ChatContent = messageContent.value) => { - if (!checkCanSend(content)) { - return; - } - const isNewChat = messageList.value.length === 0; - if (isNewChat) { - if (activeSampleId.value) { - currentSampleId = activeSampleId.value; - } - - if (activeLLMId.value) { - currentLLMId = activeLLMId.value; - } - } - let resMsgContent: ChatContent = null; - - try { - const [userItem, assistantItem] = addChatItem(content); - - resMsgContent = await questionAi(content.values); - - updateLoadIndex(); - - userItem.historyId = questionRes?.history_id; - userItem.content.values = questionRes?.question ?? userItem.content.values; - assistantItem.historyId = questionRes?.history_id; - const currentTime = formatShowTimeYear(moment().format('YYYY-MM-DD HH:mm:ss')); - assistantItem.createTime = currentTime; - assistantItem.content = resMsgContent; - setTimeout(() => { - // 鏀跺埌鍥炲锛岀户缁粴 - scrollToBottom(); - }, 300); - } catch (error: any) { - console.log(error); - } -}; - -const sendClick = () => { - sendChatMessage(messageContent.value); -}; - -const { loadRangeData, onChatListScroll, moreIsLoading, updateLoadIndex } = useScrollLoad({ - container: chatListDom, - historyGroupId: currentRouteId, - messageList, - parseAnswerContent: parseContent, +const props = defineProps({ + msgList: { + type: Array as PropType<ChatMessage[]>, + }, + isTalking: { + type: Boolean, + default: false, + }, }); -const chatListLoading = ref(true); - -const scrollToBottom = () => { - containerRef.value?.scrollToBottom(); -}; +const emit = defineEmits({ + shareClick: (msg: ChatMessage) => true, + setCommonQuestionClick: (msg: ChatMessage) => true, + sendChatMessage: (msg: ChatContent) => true, + askMoreClick: (msg: ContextHistory) => true, + stopGenClick: () => true, +}); const showAskMore = computed(() => { if (!props.msgList || props.msgList.length === 0) return false; const last = props.msgList.at(-1); @@ -504,61 +67,24 @@ const result = isShow && !isSharePage.value; return result; }); -const askMoreClick = (item) => { - if (!item.question) return; - sendChatMessage({ type: AnswerType.Text, values: item.question }); +const shareClick = (msg: ChatMessage) => { + emit('shareClick', msg); }; -//#region ====================== 鍒嗕韩 ====================== - -const shareLinkDlgRef = useCompRef(ShareLinkDlg); - -const shareClick = async (item: ChatMessage) => { - shareLinkDlgRef.value.openShare(item); +const setCommonQuestionClick = (msg: ChatMessage) => { + emit('setCommonQuestionClick', msg); }; -//#endregion -//#region ====================== 鐢ㄦ埛璇㈤棶鐨勯棶棰樿缃负甯哥敤璇� ====================== -const setCommonQuestionInfo = ref({}); - -//鐢ㄦ埛闂璁剧疆涓哄父鐢ㄨ -const setCommonQuestionClick = (item) => { - setCommonQuestionInfo.value = item; +const sendChatMessage = (msg: ChatContent) => { + emit('sendChatMessage', msg); }; -//#endregion + +const askMoreClick = (msg: ContextHistory) => { + emit('askMoreClick', msg); +}; + +const stopGenClick = () => { + emit('stopGenClick'); +}; </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; -} -</style> +<style scoped lang="scss"></style> -- Gitblit v1.9.3