From 32e433e28a3b779874f2317b80c4cef81957ff43 Mon Sep 17 00:00:00 2001
From: yangyin <1850366751@qq.com>
Date: 星期一, 14 十月 2024 16:28:24 +0800
Subject: [PATCH] 修改提问示例的样式

---
 src/components/chat/Chat.vue |  569 ++++++++++++++++++++++++++++++++++++++++----------------
 1 files changed, 409 insertions(+), 160 deletions(-)

diff --git a/src/components/chat/Chat.vue b/src/components/chat/Chat.vue
index 4856176..d853f77 100644
--- a/src/components/chat/Chat.vue
+++ b/src/components/chat/Chat.vue
@@ -1,125 +1,205 @@
 <template>
-	<div class="flex flex-col h-full">
-		<div class="h-full flex flex-col items-center overflow-y-auto">
-			<div ref="chatListDom" class="h-full w-[100ch]">
-				<div class="group flex px-4 py-4 hover:bg-slate-100 rounded-lg" v-for="(item, index) of computedMessageList" :key="index">
-					<img class="rounded-full size-12 mr-4" :src="roleImageMap[item.role]" alt="" srcset="" />
+	<div class="flex h-full">
+		<div class="flex flex-col h-full flex-auto">
+			<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" 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="`${item.historyId}_${item.role}`"
+					>
+						<img
+							class="rounded-full size-12 flex-0"
+							:class="{ 'mr-4': item.role === RoleEnum.assistant, 'ml-4': item.role === RoleEnum.user }"
+							:src="roleImageMap[item.role]"
+							alt=""
+							srcset=""
+						/>
+						<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="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">
-						<div class="relative" v-if="item.content?.values">
-							<div class="text-sm rounded-[6px] p-4 leading-relaxed max-w-[100ch] bg-white">
-								<component class="max-w-[100ch]" :is="answerTypeMapCom[item.content.type]" :data="item.content.values" />
+											<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>
+									</div>
+
+									<!-- 鎿嶄綔 -->
+									<div v-if="item.role === RoleEnum.assistant" 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"
+										>
+											<i
+												class="p-2 ywifont ywicon-copy cursor-pointer hover:text-[#0284ff] hover:!text-[18px]"
+												@click="copyClick(item)"
+											/>
+										</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>
+										</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>
+									</div>
+								</div>
+
+								<Loding v-if="isTalking && index === computedMessageList.length - 1" class="w-fit" :process="process" />
 							</div>
-
-							<!-- <div v-if="item.role === RoleEnum.assistant" class="absolute flex items-center right-0 space-x-2 mr-2 mt-2">
-								<SvgIcon class="cursor-pointer" name="ele-CopyDocument" @click="copyClick(item.content)" />
-								<SvgIcon class="cursor-pointer" name="ywicon icon-dianzan" />
-								<SvgIcon class="cursor-pointer" :size="12" name="ywicon icon-buzan" />
-							</div> -->
 						</div>
-
-						<Loding v-else />
+					</div>
+					<div v-if="showAskMore" class="ml-4 mt-5 text-sm">
+						<div class="text-gray-600 mb-5">浣犲彲浠ョ户缁棶鎴戯細</div>
+						<div class="space-y-2 inline-flex flex-col">
+							<div
+								v-for="item in computedMessageList.at(-1).content.askMoreList"
+								:key="item.history_id"
+								class="bg-white p-3 hover:bg-[#c5e0ff] hover:text-[#1c86ff] cursor-pointer rounded-lg"
+								@click="askMoreClick(item)"
+							>
+								{{ item.question }}
+							</div>
+						</div>
 					</div>
 				</div>
 			</div>
+
+			<div class="sticky bottom-0 w-full p-6 pb-8 bg-[rgb(247,248,250)] flex justify-center">
+				<PlayBar
+					v-model:voicePageIsShow="voicePageIsShow"
+					:isTalking="isTalking"
+					:isHome="false"
+					v-model="messageContent.values"
+					@sendClick="sendClick"
+					:style="{ width: chatWidth }"
+				></PlayBar>
+			</div>
 		</div>
 
-		<div class="sticky bottom-0 w-full p-6 pb-8 bg-gray-100 flex justify-center">
-			<PlayBar :isTalking="isTalking" v-model="messageContent.values" @sendClick="sendChatMessage" />
-		</div>
+		<CustomDrawer v-model:isShow="drawerIsShow" @updateChatInput="updateChatInput" />
 	</div>
 </template>
 
 <script setup lang="ts">
-import { computed, nextTick, onMounted, ref, watch } from 'vue';
-import useClipboard from 'vue-clipboard3';
+import _ from 'lodash';
+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 { RecordSet } from './model/Record';
+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 { AnswerType, RoleEnum, answerTypeMapCom, roleImageMap, type ChatMessage } from './model/types';
-import { GetHistoryAnswer, QueryHistoryDetail, QuestionAi } from '/@/api/ai/chat';
+import { AnswerState, AnswerType, RoleEnum, answerTypeMapCom, roleImageMap, type ChatMessage } from './model/types';
+import { GetHistoryAnswer, QuestionAi, extCallQuery } 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, activeRoomId } from '/@/stores/chatRoom';
-import { ElMessage } from 'element-plus';
-import { content } from 'html2canvas/dist/types/css/property-descriptors/content';
+import { activeChatRoom, activeLLMId, activeSampleId, activeSectionAId, getRoomConfig, roomConfig } from '/@/stores/chatRoom';
+import { ErrorCode } from '/@/utils/request';
 
+const chatWidth = '75%';
+const voicePageIsShow = ref(false);
 let isTalking = ref(false);
 let messageContent = ref<ChatContent>({
 	type: AnswerType.Text,
 	values: '',
 });
+const currentRoute = router.currentRoute;
+const currentRouteId = currentRoute.value.query.id as string;
 const chatListDom = ref<HTMLDivElement>();
 const messageList = ref<ChatMessage[]>([]);
 const computedMessageList = computed(() => {
-	return messageList.value.filter((v) => v.role !== RoleEnum.system);
+	return messageList.value.filter((v) => !!v);
 });
-// onMounted(() => {
-// 	if (!activeChatRoom.value) {
-// 		router.replace({
-// 			name: 'Home',
-// 		});
-// 		return;
-// 	}
-// 	messageContent.value = {
-// 		type: AnswerType.Text,
-// 		values: activeChatRoom.value.title,
-// 	};
-// 	sendChatMessage();
-// });
-
-const getAnswerById = async (historyId: string) => {
-	return await GetHistoryAnswer({
-		history_id: historyId,
-	});
-};
-let currentSectionId = '';
-watch(
-	() => activeRoomId.value,
-	async (val) => {
-		if (!val) {
-			router.replace({
-				name: 'Home',
-			});
-			return;
-		}
-
-		const res = await QueryHistoryDetail({
-			history_group_id: activeRoomId.value,
-		});
-		messageList.value = (res.details ?? []).map((item) => {
-			return {
-				role: RoleEnum.user,
-				content: {
-					type: AnswerType.Text,
-					values: item.question,
-				},
-			} as ChatMessage;
-		});
-		currentSectionId = res?.details?.[0]?.section_a_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, {
-				role: RoleEnum.assistant,
-				content: parseContent(item.answer),
-			});
-			i++;
-		});
-		
-	},
-	{
-		immediate: true,
-	}
-);
 
 const parseContent = (res) => {
+	if (!res) return null;
 	let content: ChatContent = {
 		type: AnswerType.Text,
-		values: '鍙戠敓閿欒锛�',
+		values: '瑙f瀽澶辫触锛�',
 	};
+
 	switch (res.answer_type) {
 		case AnswerType.RecordSet:
 			content = {
@@ -147,71 +227,95 @@
 				values: res.summary,
 			};
 			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: '鍙戠敓閿欒锛�',
+				values: '瑙f瀽澶辫触锛�',
 			};
 			break;
 	}
+	content.askMoreList = _.orderBy(res.context_history, [(item) => Number(item.radio)], ['desc']);
+	content.errCode = res?.err_code;
+	content.errMsg = res?.json_msg;
+	content.origin = res;
 	return content;
 };
 
-const questionAi = async (text) => {
-	const res = await QuestionAi({
-		question: text,
-		section_a_id: currentSectionId,
-		history_group_id: activeRoomId.value,
-	});
-	// const res = {
-	// 	json_ok: true,
-	// 	question: '鏄ㄦ棩浜斾竴骞垮満鍘嬪姏',
-	// 	answer_type: 'recordset',
-	// 	values: {
-	// 		names: ['yesterday', 'max_pressure'],
-	// 		values: [
-	// 			['2024-06-28 00:00:00', 24.378],
-	// 			['2024-06-29 00:00:00', 24.276],
-	// 		],
-	// 		type: 'records',
-	// 		title: '鏄ㄦ棩浜斾竴骞垮満(D_GW_04)鐨勬渶澶у帇鍔涘��',
-	// 	},
-	// };
+const { clearQueryProcess, process, processId, queryProcess } = useQueryProcess();
+const DEFAULT_SECTION_A_ID = 'knowledge_base';
 
+let questionRes = null;
+
+let finalCalcSectionAId = null;
+const questionAi = async (text) => {
+	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,
+		};
+	}
+	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,
+		question: text,
+		// FIXME: 鏆傛椂杩欐牱
+		section_a_id: currentSectionAId,
+		history_group_id: currentRouteId,
+		raw_mode: roomConfig.value?.[currentRouteId]?.isAnswerByLLM ?? false,
+		...judgeParams,
+	} as any;
+
+	if (currentSampleId) {
+		params.sample_id = currentSampleId;
+		currentSampleId = '';
+	}
+
+	// if (currentLLMId) {
+	// 	params.llm_id = currentLLMId;
+	// }
+	clearQueryProcess();
+	queryProcess();
+	const res = await QuestionAi(params).finally(() => {
+		clearQueryProcess();
+	});
+	questionRes = res;
 	const content = parseContent(res);
 	return content;
-};
-
-const sendChatMessage = async (content: ChatContent = messageContent.value) => {
-	if (!messageContent.value?.values) return;
-	if (activeChatRoom.value.isInitial) {
-		activeChatRoom.value.title = messageContent.value.values;
-		activeChatRoom.value.isInitial = false;
-	}
-	try {
-		isTalking.value = true;
-		// 鍙戦�佸綋鍓�
-		messageList.value.push({ role: RoleEnum.user, content });
-		// 娓呯┖杈撳叆妗�
-		clearMessageContent();
-		// 鍑虹幇鍥炲锛岀疆绌哄嚭鐜扮瓑寰呭姩鐢�
-		messageList.value.push({ role: RoleEnum.assistant, content: null });
-
-		let resMsgContent: ChatContent = null;
-		resMsgContent = await questionAi(content.values);
-		appendLastMessageContent(resMsgContent);
-	} catch (error: any) {
-		appendLastMessageContent({
-			type: AnswerType.Text,
-			values: '鍙戠敓閿欒锛�',
-		});
-	} finally {
-		isTalking.value = false;
-	}
-};
-
-const appendLastMessageContent = (content: ChatContent) => {
-	messageList.value.at(-1).content = content;
 };
 
 const clearMessageContent = () =>
@@ -220,36 +324,181 @@
 		values: '',
 	});
 
-const scrollToBottom = () => {
-	if (!chatListDom.value) return;
-	chatListDom.value.lastElementChild?.scrollIntoView();
+let currentSampleId = '';
+
+let currentLLMId = null;
+
+
+const sendChatMessage = async (content: ChatContent = messageContent.value, cb?: any, isCallExtParams?: any) => {
+	if (!content?.values || isTalking.value || chatListLoading.value) 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 {
+		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;
+		// 鍙戦�佸綋鍓�
+		messageList.value.push(userItem);
+		// 娓呯┖杈撳叆妗�
+		clearMessageContent();
+
+		// 鍑虹幇鍥炲锛岀疆绌哄嚭鐜扮瓑寰呭姩鐢�
+		messageList.value.push(assistantItem);
+		// 婊氬姩鑷冲綋鍓嶅彂閫佹秷鎭�
+		scrollToBottom();
+
+		if (isCallExtParams) {
+			const extRes = await extCallQuery(isCallExtParams);
+			questionRes = extRes;
+			resMsgContent = parseContent(extRes);
+		} else {
+			resMsgContent = await questionAi(content.values);
+		}
+		nextUserMsgEndIndex.value++;
+		if (isNewChat) {
+			const firstResCb = getRoomConfig(currentRouteId, 'firstResCb');
+			firstResCb?.(resMsgContent);
+		} else {
+			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,
+		// 	values: '鍙戠敓閿欒锛�',
+		// });
+	} finally {
+		isTalking.value = false;
+	}
 };
 
-watch(
-	messageList,
-	() => {
-		nextTick(() => scrollToBottom());
-	},
-	{
-		deep: true,
+const sendClick = (cb) => {
+	sendChatMessage(messageContent.value, cb);
+};
+const appendLastMessageContent = (content: ChatContent) => {
+	if (messageList.value.at(-1)) {
+		messageList.value.at(-1).content = content;
 	}
-);
+};
+const { loadRangeData, onChatListScroll, moreIsLoading, nextUserMsgEndIndex } = useScrollLoad({
+	container: chatListDom,
+	historyGroupId: currentRouteId,
+	messageList,
+	parseAnswerContent: parseContent,
+});
 
-//#region ====================== 鑱婂ぉ鍐呭鎿嶄綔 ======================
+const chatListLoading = ref(false);
 
-const { toClipboard } = useClipboard();
+const { scrollToBottom } = useScrollToBottom({
+	chatListDom: chatListDom,
+});
 
-const copyClick = (content) => {
-	ElMessage.success('澶嶅埗鎴愬姛');
-	toClipboard(content);
+onMounted(async () => {
+	messageList.value = [];
+	// 鍔犺浇鍒濆鏁版嵁
+	chatListLoading.value = true;
+
+	await loadRangeData().finally(() => {
+		chatListLoading.value = false;
+	});
+	if (messageList.value.length === 0) {
+		messageContent.value = {
+			type: AnswerType.Text,
+			values: activeChatRoom.value.title,
+		};
+
+		sendChatMessage();
+	} else {
+		setTimeout(() => {
+			// 鍒濆鐘舵�佹粴涓�涓�
+			scrollToBottom();
+
+			setTimeout(() => {
+				chatListDom.value.addEventListener('scroll', onChatListScroll);
+			}, 300);
+		}, 300);
+	}
+});
+//#region ====================== 鍏宠仈鏌ヨ ======================
+const relativeQueryClick = async (val) => {
+	sendChatMessage(
+		{
+			type: AnswerType.Text,
+			values: val.question,
+		},
+		undefined,
+		{
+			history_group_id: currentRouteId,
+			question: val.question,
+			call_ext_id: val.call_ext_id,
+			call_ext_args: val.agrs ? JSON.stringify(val.agrs) : null,
+		}
+	);
+};
+//#endregion
+
+const {
+	copyClick,
+	likeClick,
+	unLikeClick,
+	feedbackPosition,
+	feedbackIsShow,
+	feedbackContent,
+	feedbackPanelRef,
+	currentFeedbackMapItem,
+	feedbackClick,
+	askMoreClick,
+	fixQuestionClick,
+	preQuestion,
+	showFixQuestion,
+	showAskMore,
+} = useAssistantContentOpt({
+	sendChatMessage,
+	displayMessageList: computedMessageList,
+});
+
+//#region ====================== 渚ц竟鏍廳rawer ======================
+const drawerIsShow = ref(false);
+
+const updateChatInput = (content) => {
+	messageContent.value.values = content;
 };
 //#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);
+		}
+	}
+}
 </style>

--
Gitblit v1.9.3