From 91142fc15ee81cdc3f80134e5e8faaff237f1e53 Mon Sep 17 00:00:00 2001
From: wujingjing <gersonwu@qq.com>
Date: 星期一, 04 十一月 2024 11:13:36 +0800
Subject: [PATCH] 简单流式展示

---
 src/components/chat/Chat.vue |  306 +++++++++++++++++++++++++++++++++++---------------
 1 files changed, 213 insertions(+), 93 deletions(-)

diff --git a/src/components/chat/Chat.vue b/src/components/chat/Chat.vue
index 190693c..3f9d9aa 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,6 +23,21 @@
 						/>
 						<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="isTalking && index === computedMessageList.length - 1">
+									<div class="text-sm rounded-[6px] p-4 leading-relaxed bg-white"
+										 v-if="item.role === RoleEnum.assistant">
+										<!-- 杩囩▼杈撳嚭 -->
+										<el-steps direction="vertical" :active="activeStep">
+											<!-- <el-step title="process" status="process" />
+													<el-step title="success" status="success" />
+													<el-step title="wait" status="wait" />
+													<el-step title="finish" status="finish" />
+													<el-steps title="error" status="error" /> -->
+
+											<el-step v-for="item in stepList" :title="item.title" :status="stepEnumMap[item.status]" />
+										</el-steps>
+									</div>
+								</div>
 								<div class="w-full" v-if="item.content?.values">
 									<div
 										class="text-sm rounded-[6px] p-4 leading-relaxed"
@@ -72,7 +92,7 @@
 											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]"
+												class="p-2 ywifont ywicon-copy cursor-pointer hover:text-[#0284ff] hover:!text-[18px]"
 												@click="copyClick(item)"
 											/>
 										</div>
@@ -80,14 +100,14 @@
 											<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]"
+													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 ywicon icon-buzan cursor-pointer hover:text-[#0284ff] !text-[13px] hover:!text-[15px]"
+													class="p-2 ywifont ywicon-buzan cursor-pointer hover:text-[#0284ff] !text-[13px] hover:!text-[15px]"
 													@click="unLikeClick(item)"
 												/>
 											</div>
@@ -95,7 +115,7 @@
 
 										<div class="flex items-center justify-center size-[15px] relative">
 											<i
-												class="p-2 ywicon icon-wentifankui cursor-pointer hover:text-[#0284ff] !text-[13px] hover:!text-[15px]"
+												class="p-2 ywifont ywicon-wentifankui cursor-pointer hover:text-[#0284ff] !text-[13px] hover:!text-[15px]"
 												@click="
 													($event) =>
 														feedbackClick(
@@ -118,12 +138,10 @@
 										</div>
 									</div>
 								</div>
-
-								<Loding v-else 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,7 +157,7 @@
 				</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"
@@ -156,22 +174,32 @@
 </template>
 
 <script setup lang="ts">
-import { ElMessage } from 'element-plus';
 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 { 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 { QuestionAi, 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 { ErrorCode } from '/@/utils/request';
 
 const chatWidth = '75%';
@@ -183,12 +211,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,41 +276,115 @@
 	content.origin = res;
 	return content;
 };
-
 const { clearQueryProcess, process, processId, queryProcess } = useQueryProcess();
 
+//#region ====================== 姝ラ step ======================
+const enum StepEnum {
+	Loading,
+	Success,
+	Error,
+}
+const stepEnumMap = {
+	[StepEnum.Loading]: 'process',
+	[StepEnum.Success]: 'success',
+	[StepEnum.Error]: 'error',
+};
+
+type StepItem = {
+	title: string;
+	status: StepEnum;
+};
+const activeStep = ref(-1);
+const stepList = ref<StepItem[]>([]);
+
+const resetStep = () => {
+	activeStep.value = -1;
+	stepList.value = [];
+};
+//#endregion
+
+const DEFAULT_SECTION_A_ID = 'knowledge_base';
 let questionRes = null;
+
+let finalCalcSectionAId = null;
 const questionAi = async (text) => {
-	if (!currentSectionId) {
-		ElMessage.warning('鍙戦�佸け璐ワ紝鏈‘瀹氬簲鐢ㄥ満鏅紒');
+	// 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,
+		// 	  }
+		// 	: {};
+		// 姝e父鍥炵瓟鏆傛椂涓嶉噰鐢�
+		judgeParams = {};
+	} else {
+		judgeParams = {
+			prev_question: preQuestion.value,
+		};
 	}
-	processId.value = uuidv4();
-	const judgeParams = !preQuestion.value
-		? {}
-		: {
-				prev_question: preQuestion.value,
-		  };
+	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.value,
+		// 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 (currentSampleId) {
-		params.sample_id = currentSampleId;
+	if (activeGroupType.value) {
+		params.group_type = activeGroupType.value;
 	}
 
-	if (currentLLMId) {
-		params.llm_id = currentLLMId;
+	if (currentSampleId) {
+		params.sample_id = currentSampleId;
+		currentSampleId = '';
 	}
-	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 {
+			switch (chunkRes.mode) {
+				case 'begin':
+					break;
+				case 'end':
+					break;
+			}
+
+			stepList.value.push({
+				title: chunkRes.value,
+				status: StepEnum.Success,
+			});
+			scrollToBottom();
+
+			// process.value = chunkRes.value;
+		}
+	}).finally(() => {
+		resetStep();
 	});
 	questionRes = res;
 	const content = parseContent(res);
@@ -295,19 +397,12 @@
 		values: '',
 	});
 
-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, cb?: any, isCallExtParams?: any) => {
-	if (!content?.values) return;
+	if (!content?.values || isTalking.value || chatListLoading.value) return;
 	const isNewChat = messageList.value.length === 0;
 	if (isNewChat) {
 		if (activeSampleId.value) {
@@ -316,10 +411,6 @@
 
 		if (activeLLMId.value) {
 			currentLLMId = activeLLMId.value;
-		}
-
-		if (activeSectionAId.value) {
-			currentSectionId = activeSectionAId.value;
 		}
 	}
 	let resMsgContent: ChatContent = null;
@@ -335,6 +426,9 @@
 
 		// 鍑虹幇鍥炲锛岀疆绌哄嚭鐜扮瓑寰呭姩鐢�
 		messageList.value.push(assistantItem);
+		// 婊氬姩鑷冲綋鍓嶅彂閫佹秷鎭�
+		scrollToBottom();
+
 		if (isCallExtParams) {
 			const extRes = await extCallQuery(isCallExtParams);
 			questionRes = extRes;
@@ -342,7 +436,7 @@
 		} else {
 			resMsgContent = await questionAi(content.values);
 		}
-
+		nextUserMsgEndIndex.value++;
 		if (isNewChat) {
 			const firstResCb = getRoomConfig(currentRouteId, 'firstResCb');
 			firstResCb?.(resMsgContent);
@@ -350,8 +444,14 @@
 			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,
@@ -366,51 +466,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 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,
-			item.answer === null
-				? null
-				: {
-						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,
@@ -418,14 +500,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(
@@ -460,7 +545,6 @@
 	showFixQuestion,
 	showAskMore,
 } = useAssistantContentOpt({
-	forbidScroll,
 	sendChatMessage,
 	displayMessageList: computedMessageList,
 });
@@ -474,10 +558,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