From 1d0eec6da86e3e9bcbf002d1a22142586fe023a3 Mon Sep 17 00:00:00 2001
From: wujingjing <gersonwu@qq.com>
Date: 星期二, 31 十二月 2024 11:34:15 +0800
Subject: [PATCH] 拆分组件 assistantMsg.vue

---
 src/components/chat/Chat.vue                        |  335 +--------------
 src/components/chat/hooks/useAssistantContentOpt.ts |   14 
 src/components/chat/messageList/index.vue           |  564 ++++++++++++++++++++++++++
 src/components/chat/assistant/index.vue             |  333 +++++++++++++++
 src/components/chat/user/index.vue                  |    7 
 5 files changed, 930 insertions(+), 323 deletions(-)

diff --git a/src/components/chat/Chat.vue b/src/components/chat/Chat.vue
index 4cad518..ff4ec3f 100644
--- a/src/components/chat/Chat.vue
+++ b/src/components/chat/Chat.vue
@@ -12,267 +12,20 @@
 				<div v-for="(item, msgIndex) of computedMessageList" :key="`${item.historyId}_${item.role}`">
 					<UserMsg
 						:msg="item"
-						@copyMsg="copyClick"
 						@shareClick="shareClick"
 						@setCommonQuestion="setCommonQuestionClick"
 						v-if="item.role === RoleEnum.user"
 					></UserMsg>
 
-					<div v-else class="flex px-4 py-6 rounded-lg relative" :class="{ 'px-10': isShareCheck }">
-						<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 }"
-							:src="roleImageMap[item.role]"
-							alt=""
-							srcset=""
-						/>
-						<div class="flex-auto flex">
-							<div class="inline-flex flex-col" :class="{ 'w-full': item.role === RoleEnum.assistant }">
-								<div class="w-full">
-									<div class="rounded-[6px] p-4 leading-relaxed bg-white">
-										<!-- #region ====================== 娑堟伅鍐呭 ======================-->
-										<!-- <template v-if="item.content?.values"> -->
-										<!-- #region ====================== 鎶ラ敊淇℃伅 ======================-->
-										<div v-if="item.content?.errCode === ErrorCode.Message" class="flex-column w-full">
-											<p class="text-danger">
-												{{ item.content.errMsg }}
-											</p>
-											<div class="mt-3 flex" v-if="showFixQuestion(item)">
-												<div class="text-gray-600 flex-0 mb-auto py-3">
-													{{ '鐚滀綘鎯抽棶锛�' }}
-												</div>
-												<div class="flex-auto space-x-2 space-y-1 inline-flex flex-wrap items-center">
-													<div
-														v-for="fixItem in item.content.origin?.sample_question"
-														:key="fixItem"
-														class="bg-gray-200 p-3 hover:bg-[#c5e0ff] hover:text-[#1c86ff] cursor-pointer rounded-lg first-of-type:ml-2 first-of-type:mt-1"
-														@click="fixQuestionClick(fixItem, item.content.origin)"
-													>
-														{{ fixItem }}
-													</div>
-												</div>
-											</div>
-										</div>
-										<!-- #endregion -->
-										<!-- #region ====================== 鍥炵瓟缁勪欢 ======================-->
-										<template v-else>
-											<template v-if="item.content.type === AnswerType.Report">
-												<template v-if="item?.stepGroup?.length > 0">
-													<div v-for="(num, index) in item?.stepGroup?.length" :key="index">
-														<!-- #region ====================== 鎰忓浘鍒嗘瀽 ======================-->
-														<div class="flex flex-col" v-if="item?.stepGroup?.[index]?.value?.length > 0">
-															<!-- #region ====================== 鎰忓浘鍒嗘瀽 ======================-->
-															<div class="flex items-center">
-																<span class="mr-2">鎰忓浘鍒嗘瀽锛�</span>
-																<div
-																	@click="toggleStepList(item?.stepGroup?.[index])"
-																	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?.stepGroup?.[index]) }}
-																	</span>
-																	<span
-																		class="ywifont"
-																		:class="{
-																			'ywicon-unfold': !item?.stepGroup?.[index].isShow,
-																			'ywicon-fold': item?.stepGroup?.[index].isShow,
-																		}"
-																	></span>
-																</div>
-															</div>
-															<!-- #endregion -->
-
-															<!-- #region ====================== 杩囩▼杈撳嚭 ======================-->
-															<el-steps
-																v-show="item?.stepGroup?.[index].isShow"
-																class="mt-3"
-																direction="vertical"
-																:active="activeStep"
-															>
-																<el-step
-																	:key="`template-${stepIndex}`"
-																	v-for="(subItem, stepIndex) in item?.stepGroup?.[index].value"
-																	:title="subItem.title"
-																	:status="stepEnumMap[subItem.status]"
-																>
-																	<template
-																		#icon
-																		v-if="
-																			stepIndex + 1 === item?.stepGroup?.[index].value.length &&
-																			isTalking &&
-																			msgIndex === computedMessageList.length - 1
-																		"
-																	>
-																		<span class="ywifont ywicon-loading1 animate-spin !text-[24px]"></span>
-																	</template>
-																	<template #title>
-																		<span class="">
-																			{{ subItem.title }}
-
-																			<span v-if="subItem.ms" class="text-green-600">{{ `锛�${subItem.ms}锛塦 }}</span></span
-																		>
-																	</template>
-
-																	<template #description v-if="subItem?.subStep?.length > 0">
-																		<div class="my-1 flex flex-col gap-1 text-[14px]">
-																			<div
-																				:key="`${item.historyId}-${stepIndex + 1}-${multiChatIndex + 1}`"
-																				v-for="(multiChatItem, multiChatIndex) in subItem.subStep"
-																			>
-																				<component
-																					v-if="multiChatItem.type === MultiChatType.Select"
-																					:order="`${stepIndex + 1}-${multiChatIndex + 1}`"
-																					:item="multiChatItem"
-																					:is="multiChatTypeMapCom[multiChatItem.type]"
-																					:disabled="
-																						!(
-																							stepIndex + 1 === item?.stepGroup?.[index].value.length &&
-																							isTalking &&
-																							msgIndex === computedMessageList.length - 1
-																						)
-																					"
-																				/>
-																				<component
-																					v-else-if="multiChatItem.type === MultiChatType.Result"
-																					:is="answerTypeMapCom['summary']"
-																					:data="multiChatItem.data.content.values"
-																					:originData="multiChatItem.data"
-																				/>
-																				<div v-else-if="multiChatItem.type === MultiChatType.Summary" class="ml-4 mt-5 pb-10">
-																					<div class="text-gray-600 mb-5">浣犲彲浠ョ户缁棶鎴戯細</div>
-																					<div class="space-y-2 inline-flex flex-col">
-																						<div
-																							v-for="item in multiChatItem.data.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>
-																	</template>
-																</el-step>
-															</el-steps>
-															<!-- #endregion -->
-														</div>
-														<!-- #endregion -->
-
-														<component
-															v-if="item.content?.values?.[index]"
-															:reportIndex="index"
-															:conclusion="item.content.values[index].conclusion"
-															:is="answerTypeMapCom[item.content.values[index].content.type]"
-															:data="item.content.values[index].content.values"
-															:originData="item.content.values[index]"
-															:historyId="item.historyId"
-															:isTalking="isTalking && msgIndex === computedMessageList.length - 1"
-														/>
-													</div>
-												</template>
-												<p v-else class="text-info">鏆傛棤鍐呭锛岃閲嶈瘯</p>
-											</template>
-											<component
-												v-else
-												:historyId="item.historyId"
-												:conclusion="item.conclusion"
-												:is="answerTypeMapCom[item.content.type]"
-												:data="item.content.values"
-												:originData="item"
-												:isTalking="isTalking && msgIndex === computedMessageList.length - 1"
-											/>
-										</template>
-										<!-- #endregion -->
-										<!-- </template> -->
-
-										<!-- #endregion -->
-										<!-- #region ====================== 闄勫姞鍐呭 ======================-->
-										<!-- #region ====================== 鍋滄 ======================-->
-										<span v-if="item.isStopMsg && item?.role === RoleEnum.assistant" class="text-gray-400 text-[12px]"
-											>锛堝凡鍋滄锛�</span
-										>
-										<!-- parseContent 杩斿洖涓� null -->
-										<p v-if="!item.content && !isTalking && !item.isStopMsg" class="text-red-500">鏆傛棤鏁版嵁</p>
-										<!-- #endregion -->
-										<!-- #endregion -->
-									</div>
-									<!-- #region ====================== ai 娑堟伅鎿嶄綔 ======================-->
-									<div
-										v-if="item.role === RoleEnum.assistant && item.content?.values && !isSharePage && !isShareCheck"
-										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">
-											<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>
-										<el-tooltip effect="dark" content="鍒嗕韩" placement="top">
-											<div class="flex items-center justify-center size-[15px]">
-												<i
-													class="p-2 ywifont ywicon-fenxiang cursor-pointer hover:text-[#0284ff] !text-[15px] hover:!text-[18px]"
-													@click="shareClick(item)"
-												/>
-											</div>
-										</el-tooltip>
-										<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>
-									<!-- #endregion -->
-								</div>
-							</div>
-						</div>
-					</div>
+					<AssistantMsg
+						v-else
+						:msg="item"
+						:msgList="computedMessageList"
+						:isLast="msgIndex === computedMessageList.length - 1"
+						@sendChatMessage="sendChatMessage"
+						@shareMsg="shareClick"
+						:isTalking="isTalking"
+					/>
 				</div>
 				<div v-if="showAskMore" class="ml-4 mt-5 pb-10">
 					<div class="text-gray-600 mb-5">浣犲彲浠ョ户缁棶鎴戯細</div>
@@ -322,35 +75,23 @@
 <script setup lang="ts">
 import type { CancelTokenSource } from 'axios';
 import axios from 'axios';
-import { findLast, orderBy } from 'lodash-es';
+import { orderBy } from 'lodash-es';
 import moment from 'moment';
 import { computed, onActivated, onMounted, ref } from 'vue';
-import useClipboard from 'vue-clipboard3';
 import { loadAmisSource } from '../amis/load';
-import FeedbackPanel from './components/FeedbackPanel.vue';
-import { useAssistantContentOpt } from './hooks/useAssistantContentOpt';
 import { convertProcessItem, convertProcessToStep, formatShowTimeYear, useScrollLoad } from './hooks/useScrollLoad';
 import type { ChatContent } from './model/types';
-import {
-	AnswerState,
-	AnswerType,
-	MultiChatType,
-	RoleEnum,
-	answerTypeMapCom,
-	roleImageMap,
-	stepEnumMap,
-	type ChatMessage,
-} from './model/types';
-import { extCallQuery, getShareChatJsonByPost, questionStreamByPost } from '/@/api/ai/chat';
+import { AnswerState, AnswerType, RoleEnum, type ChatMessage } from './model/types';
+import { getShareChatJsonByPost, 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 { ElMessage } from 'element-plus';
+import AssistantMsg from './assistant/index.vue';
 import ChatContainer from './components/ChatContainer.vue';
 import ShareLinkDlg from './components/shareLink/index.vue';
 import UserMsg from './user/index.vue';
-import { multiChatTypeMapCom } from '/@/components/chat/chatComponents/multiChat';
 import router from '/@/router';
 import {
 	activeChatRoom,
@@ -362,7 +103,6 @@
 	roomConfig,
 } from '/@/stores/chatRoom';
 import emitter from '/@/utils/mitt';
-import { ErrorCode } from '/@/utils/request';
 import { useCompRef } from '/@/utils/types';
 import { toMyFixed } from '/@/utils/util';
 const containerRef = useCompRef(ChatContainer);
@@ -488,22 +228,9 @@
 	return content;
 };
 
-//#region ====================== 姝ラ step ======================
-const activeStep = ref(-1);
-
-const resetStep = () => {
-	activeStep.value = -1;
-};
-
-const toggleStepLabel = (item: any) => (item.isShow ? '鏀惰捣' : '灞曞紑');
-const toggleStepList = (item: any) => {
-	item.isShow = !item.isShow;
-};
-
-//#endregion
-
 let questionRes = null;
 let position = null;
+const preQuestion = ref(null);
 
 let lastAxiosSource: CancelTokenSource = null;
 const questionAi = async (text) => {
@@ -538,7 +265,6 @@
 		currentSampleId = '';
 	}
 
-	resetStep();
 	let lastTimestamp = new Date().getTime();
 	questionRes = {};
 	let lastIsResult = false;
@@ -706,8 +432,6 @@
 				computedMessageList.value.at(-1).stepGroup.forEach((item) => {
 					item.isShow = false;
 				});
-
-				resetStep();
 			});
 	});
 
@@ -730,7 +454,7 @@
 	lastAxiosSource?.cancel();
 	isTalking.value = false;
 	chatListLoading.value = false;
-	resetStep();
+
 	computedMessageList.value.at(-1).isStopMsg = true;
 };
 
@@ -942,25 +666,17 @@
 	messageContent.value.values = history_data.value[currentIndex.value].content.values;
 };
 //#endregion
-const {
-	copyClick,
-	likeClick,
-	unLikeClick,
-	feedbackPosition,
-	feedbackIsShow,
-	feedbackContent,
-	feedbackPanelRef,
-	currentFeedbackMapItem,
-	feedbackClick,
-	askMoreClick,
-	fixQuestionClick,
-	preQuestion,
-	showFixQuestion,
-	showAskMore,
-} = useAssistantContentOpt({
-	sendChatMessage,
-	displayMessageList: computedMessageList,
+const showAskMore = computed(() => {
+	if (!computedMessageList.value || computedMessageList.value.length === 0) return false;
+	const last = computedMessageList.value.at(-1);
+	const isShow = last?.role === RoleEnum.assistant && last?.content?.values && last.content?.askMoreList?.length > 0;
+	const result = isShow && !isSharePage.value;
+	return result;
 });
+const askMoreClick = (item) => {
+	if (!item.question) return;
+	sendChatMessage({ type: AnswerType.Text, values: item.question });
+};
 
 //#region ====================== 渚ц竟鏍廳rawer ======================
 const drawerIsShow = ref(false);
@@ -981,7 +697,6 @@
 //#region ====================== 鍒嗕韩 ======================
 
 const shareLinkDlgRef = useCompRef(ShareLinkDlg);
-const isShareCheck = ref(false);
 
 const shareClick = async (item: ChatMessage) => {
 	shareLinkDlgRef.value.openShare(item);
diff --git a/src/components/chat/assistant/index.vue b/src/components/chat/assistant/index.vue
new file mode 100644
index 0000000..693e567
--- /dev/null
+++ b/src/components/chat/assistant/index.vue
@@ -0,0 +1,333 @@
+<template>
+	<div class="flex px-4 py-6 rounded-lg relative">
+		<div class="absolute top-0 left-[72px] text-[#8d8e99]">{{ msg?.createTime }}</div>
+
+		<img
+			class="rounded-full size-12 flex-0"
+			:class="{ 'mr-4': msg.role === RoleEnum.assistant }"
+			:src="roleImageMap[msg.role]"
+			alt=""
+			srcset=""
+		/>
+		<div class="flex-auto flex">
+			<div class="inline-flex flex-col" :class="{ 'w-full': msg.role === RoleEnum.assistant }">
+				<div class="w-full">
+					<div class="rounded-[6px] p-4 leading-relaxed bg-white">
+						<!-- #region ====================== 娑堟伅鍐呭 ======================-->
+						<!-- <template v-if="item.content?.values"> -->
+						<!-- #region ====================== 鎶ラ敊淇℃伅 ======================-->
+						<div v-if="msg.content?.errCode === ErrorCode.Message" class="flex-column w-full">
+							<p class="text-danger">
+								{{ msg.content.errMsg }}
+							</p>
+							<div class="mt-3 flex" v-if="showFixQuestion(msg)">
+								<div class="text-gray-600 flex-0 mb-auto py-3">
+									{{ '鐚滀綘鎯抽棶锛�' }}
+								</div>
+								<div class="flex-auto space-x-2 space-y-1 inline-flex flex-wrap items-center">
+									<div
+										v-for="fixItem in msg.content.origin?.sample_question"
+										:key="fixItem"
+										class="bg-gray-200 p-3 hover:bg-[#c5e0ff] hover:text-[#1c86ff] cursor-pointer rounded-lg first-of-type:ml-2 first-of-type:mt-1"
+										@click="fixQuestionClick(fixItem, msg.content.origin)"
+									>
+										{{ fixItem }}
+									</div>
+								</div>
+							</div>
+						</div>
+						<!-- #endregion -->
+						<!-- #region ====================== 鍥炵瓟缁勪欢 ======================-->
+						<template v-else>
+							<template v-if="msg.content.type === AnswerType.Report">
+								<template v-if="msg?.stepGroup?.length > 0">
+									<div v-for="(num, index) in msg?.stepGroup?.length" :key="index">
+										<!-- #region ====================== 鎰忓浘鍒嗘瀽 ======================-->
+										<div class="flex flex-col" v-if="msg?.stepGroup?.[index]?.value?.length > 0">
+											<!-- #region ====================== 鎰忓浘鍒嗘瀽 ======================-->
+											<div class="flex items-center">
+												<span class="mr-2">鎰忓浘鍒嗘瀽锛�</span>
+												<div
+													@click="toggleStepList(msg?.stepGroup?.[index])"
+													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(msg?.stepGroup?.[index]) }}
+													</span>
+													<span
+														class="ywifont"
+														:class="{
+															'ywicon-unfold': !msg?.stepGroup?.[index].isShow,
+															'ywicon-fold': msg?.stepGroup?.[index].isShow,
+														}"
+													></span>
+												</div>
+											</div>
+											<!-- #endregion -->
+
+											<!-- #region ====================== 杩囩▼杈撳嚭 ======================-->
+											<el-steps v-show="msg?.stepGroup?.[index].isShow" class="mt-3" direction="vertical">
+												<el-step
+													:key="`template-${stepIndex}`"
+													v-for="(subItem, stepIndex) in msg?.stepGroup?.[index].value"
+													:title="subItem.title"
+													:status="stepEnumMap[subItem.status]"
+												>
+													<template #icon v-if="stepIndex + 1 === msg?.stepGroup?.[index].value.length && isTalking && isLast">
+														<span class="ywifont ywicon-loading1 animate-spin !text-[24px]"></span>
+													</template>
+													<template #title>
+														<span class="">
+															{{ subItem.title }}
+
+															<span v-if="subItem.ms" class="text-green-600">{{ `锛�${subItem.ms}锛塦 }}</span></span
+														>
+													</template>
+
+													<template #description v-if="subItem?.subStep?.length > 0">
+														<div class="my-1 flex flex-col gap-1 text-[14px]">
+															<div
+																:key="`${msg.historyId}-${stepIndex + 1}-${multiChatIndex + 1}`"
+																v-for="(multiChatItem, multiChatIndex) in subItem.subStep"
+															>
+																<component
+																	v-if="multiChatItem.type === MultiChatType.Select"
+																	:order="`${stepIndex + 1}-${multiChatIndex + 1}`"
+																	:item="multiChatItem"
+																	:is="multiChatTypeMapCom[multiChatItem.type]"
+																	:disabled="!(stepIndex + 1 === msg?.stepGroup?.[index].value.length && isTalking && isLast)"
+																/>
+																<component
+																	v-else-if="multiChatItem.type === MultiChatType.Result"
+																	:is="answerTypeMapCom['summary']"
+																	:data="multiChatItem.data.content.values"
+																	:originData="multiChatItem.data"
+																/>
+															</div>
+														</div>
+													</template>
+												</el-step>
+											</el-steps>
+											<!-- #endregion -->
+										</div>
+										<!-- #endregion -->
+
+										<component
+											v-if="msg.content?.values?.[index]"
+											:reportIndex="index"
+											:conclusion="msg.content.values[index].conclusion"
+											:is="answerTypeMapCom[msg.content.values[index].content.type]"
+											:data="msg.content.values[index].content.values"
+											:originData="msg.content.values[index]"
+											:historyId="msg.historyId"
+											:isTalking="isTalking && isLast"
+										/>
+									</div>
+								</template>
+								<p v-else class="text-info">鏆傛棤鍐呭锛岃閲嶈瘯</p>
+							</template>
+							<component
+								v-else
+								:historyId="msg.historyId"
+								:conclusion="msg.conclusion"
+								:is="answerTypeMapCom[msg.content.type]"
+								:data="msg.content.values"
+								:originData="msg"
+								:isTalking="isTalking && isLast"
+							/>
+						</template>
+						<!-- #endregion -->
+						<!-- </template> -->
+
+						<!-- #endregion -->
+						<!-- #region ====================== 闄勫姞鍐呭 ======================-->
+						<!-- #region ====================== 鍋滄 ======================-->
+						<span v-if="msg.isStopMsg && msg?.role === RoleEnum.assistant" class="text-gray-400 text-[12px]">锛堝凡鍋滄锛�</span>
+						<!-- parseContent 杩斿洖涓� null -->
+						<p v-if="!msg.content && !isTalking && !msg.isStopMsg" class="text-red-500">鏆傛棤鏁版嵁</p>
+						<!-- #endregion -->
+						<!-- #endregion -->
+					</div>
+					<!-- #region ====================== ai 娑堟伅鎿嶄綔 ======================-->
+					<div
+						v-if="msg.role === RoleEnum.assistant && msg.content?.values && !isSharePage"
+						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="msg.content?.type === AnswerType.Text || msg.content?.type === AnswerType.Knowledge"
+						>
+							<i class="p-2 ywifont ywicon-copy cursor-pointer hover:text-[#0284ff] hover:!text-[18px]" @click="copyClick(msg)" />
+						</div>
+						<template v-if="msg.content.errCode !== ErrorCode.Message">
+							<el-tooltip effect="dark" content="鐐硅禐" placement="top">
+								<div class="flex items-center justify-center size-[15px]">
+									<i
+										:class="{ 'text-[#0284ff]': msg.state === AnswerState.Like }"
+										class="p-2 ywifont ywicon-dianzan cursor-pointer hover:text-[#0284ff] font-medium hover:!text-[18px]"
+										@click="likeClick(msg)"
+									/>
+								</div>
+							</el-tooltip>
+							<el-tooltip effect="dark" content="鐐硅俯" placement="top">
+								<div class="flex items-center justify-center size-[15px]">
+									<i
+										:class="{ 'text-[#0284ff]': msg.state === AnswerState.Unlike }"
+										class="p-2 ywifont ywicon-buzan cursor-pointer hover:text-[#0284ff] !text-[13px] hover:!text-[15px]"
+										@click="unLikeClick(msg)"
+									/>
+								</div>
+							</el-tooltip>
+						</template>
+						<el-tooltip effect="dark" content="鍒嗕韩" placement="top">
+							<div class="flex items-center justify-center size-[15px]">
+								<i
+									class="p-2 ywifont ywicon-fenxiang cursor-pointer hover:text-[#0284ff] !text-[15px] hover:!text-[18px]"
+									@click="shareClick(msg)"
+								/>
+							</div>
+						</el-tooltip>
+						<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,
+												msg,
+												msgList.filter((v) => v.role === RoleEnum.assistant).findIndex((v) => v.historyId === msg.historyId)
+											)
+									"
+								/>
+								<FeedbackPanel
+									v-show="feedbackIsShow && currentFeedbackMapItem === msg"
+									ref="feedbackPanelRef"
+									v-model:isShow="feedbackIsShow"
+									v-model:content="feedbackContent"
+									:chatItem="currentFeedbackMapItem"
+									:position="feedbackPosition"
+								/>
+							</div>
+						</el-tooltip>
+					</div>
+					<!-- #endregion -->
+				</div>
+			</div>
+		</div>
+	</div>
+</template>
+
+<script setup lang="ts" name="AssistantMsg">
+import type { PropType } from 'vue';
+import { useAssistantContentOpt } from '../hooks/useAssistantContentOpt';
+import type { ChatContent } from '../model/types';
+import {
+	AnswerState,
+	AnswerType,
+	MultiChatType,
+	RoleEnum,
+	answerTypeMapCom,
+	roleImageMap,
+	stepEnumMap,
+	type ChatMessage,
+} from '../model/types';
+import FeedbackPanel from '../components/FeedbackPanel.vue';
+
+import { multiChatTypeMapCom } from '/@/components/chat/chatComponents/multiChat';
+import { isSharePage } from '/@/stores/chatRoom';
+import { ErrorCode } from '/@/utils/request';
+
+const props = defineProps({
+	/** @description 褰撳墠娑堟伅 */
+	msg: {
+		type: Object as PropType<ChatMessage>,
+	},
+	msgList: {
+		type: Array as PropType<Array<ChatMessage>>,
+	},
+	isLast: {
+		type: Boolean,
+		default: false,
+	},
+	isTalking: {
+		type: Boolean,
+		default: false,
+	},
+});
+
+const emit = defineEmits({
+	sendChatMessage: (content: ChatContent) => true,
+	shareMsg: (msg: ChatMessage) => true,
+});
+
+const sendChatMessage = (content: ChatContent) => {
+	emit('sendChatMessage', content);
+};
+
+//#region ====================== 姝ラ step ======================
+
+const toggleStepLabel = (item: any) => (item.isShow ? '鏀惰捣' : '灞曞紑');
+const toggleStepList = (item: any) => {
+	item.isShow = !item.isShow;
+};
+
+//#endregion
+
+const {
+	copyClick,
+	likeClick,
+	unLikeClick,
+	feedbackPosition,
+	feedbackIsShow,
+	feedbackContent,
+	feedbackPanelRef,
+	currentFeedbackMapItem,
+	feedbackClick,
+	fixQuestionClick,
+	showFixQuestion,
+} = useAssistantContentOpt({
+	sendChatMessage,
+});
+
+//#region ====================== 鍒嗕韩 ======================
+const shareClick = async (item: ChatMessage) => {
+	emit('shareMsg', item);
+};
+//#endregion
+</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>
diff --git a/src/components/chat/hooks/useAssistantContentOpt.ts b/src/components/chat/hooks/useAssistantContentOpt.ts
index 13b56ca..ff2ec9f 100644
--- a/src/components/chat/hooks/useAssistantContentOpt.ts
+++ b/src/components/chat/hooks/useAssistantContentOpt.ts
@@ -10,13 +10,11 @@
 
 export type AssistantContentOptOption = {
 	sendChatMessage: any;
-	displayMessageList: ComputedRef<ChatMessage[]>;
 };
 
 export const useAssistantContentOpt = (option: AssistantContentOptOption) => {
-	const { sendChatMessage, displayMessageList } = option;
+	const { sendChatMessage } = option;
 	const { copy } = useClipboard();
-	const preQuestion = ref(null);
 
 	const copyClick = (item) => {
 		const type = item.content.type;
@@ -86,13 +84,7 @@
 	// 		feedbackContent.value = '';
 	// 	}
 	// );
-	const showAskMore = computed(() => {
-		if (!displayMessageList.value || displayMessageList.value.length === 0) return false;
-		const last = displayMessageList.value.at(-1);
-		const isShow = last?.role === RoleEnum.assistant && last?.content?.values && last.content?.askMoreList?.length > 0;
-		const result =  isShow && !isSharePage.value;
-		return result;
-	});
+
 
 	const showFixQuestion = (item) => {
 		const isShow = item?.role === RoleEnum.assistant  && item.content?.origin?.sample_question?.length > 0 && !isSharePage.value;
@@ -129,8 +121,6 @@
 		feedbackClick,
 		askMoreClick,
 		fixQuestionClick,
-		preQuestion,
-		showAskMore,
 		showFixQuestion,
 	};
 };
diff --git a/src/components/chat/messageList/index.vue b/src/components/chat/messageList/index.vue
new file mode 100644
index 0000000..1491d1d
--- /dev/null
+++ b/src/components/chat/messageList/index.vue
@@ -0,0 +1,564 @@
+<template>
+	<div class="message-list">
+		<div v-for="(item, msgIndex) of msgList" :key="`${item.historyId}_${item.role}`">
+			<UserMsg
+				:msg="item"
+				@shareClick="shareClick"
+				@setCommonQuestion="setCommonQuestionClick"
+				v-if="item.role === RoleEnum.user"
+			></UserMsg>
+
+			<AssistantMsg
+				v-else
+				:msg="item"
+				:msgList="msgList"
+				:isLast="msgIndex === msgList.length - 1"
+				@sendChatMessage="sendChatMessage"
+				@shareMsg="shareClick"
+				:isTalking="isTalking"
+			/>
+		</div>
+		<div v-if="showAskMore" class="ml-4 mt-5 pb-10">
+			<div class="text-gray-600 mb-5">浣犲彲浠ョ户缁棶鎴戯細</div>
+			<div class="space-y-2 inline-flex flex-col">
+				<div
+					v-for="item in msgList.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>
+</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 AssistantMsg from '../assistant/index.vue';
+import ChatContainer from '../components/ChatContainer.vue';
+import ShareLinkDlg from '../components/shareLink/index.vue';
+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);
+
+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 chatListLoading = ref(true);
+
+const scrollToBottom = () => {
+	containerRef.value?.scrollToBottom();
+};
+const showAskMore = computed(() => {
+	if (!props.msgList || props.msgList.length === 0) return false;
+	const last = props.msgList.at(-1);
+	const isShow = last?.role === RoleEnum.assistant && last?.content?.values && last.content?.askMoreList?.length > 0;
+	const result = isShow && !isSharePage.value;
+	return result;
+});
+const askMoreClick = (item) => {
+	if (!item.question) return;
+	sendChatMessage({ type: AnswerType.Text, values: item.question });
+};
+
+//#region ====================== 鍒嗕韩 ======================
+
+const shareLinkDlgRef = useCompRef(ShareLinkDlg);
+
+const shareClick = async (item: ChatMessage) => {
+	shareLinkDlgRef.value.openShare(item);
+};
+//#endregion
+
+//#region ====================== 鐢ㄦ埛璇㈤棶鐨勯棶棰樿缃负甯哥敤璇� ======================
+const setCommonQuestionInfo = ref({});
+
+//鐢ㄦ埛闂璁剧疆涓哄父鐢ㄨ
+const setCommonQuestionClick = (item) => {
+	setCommonQuestionInfo.value = item;
+};
+//#endregion
+</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>
diff --git a/src/components/chat/user/index.vue b/src/components/chat/user/index.vue
index cd8919d..f6a79e3 100644
--- a/src/components/chat/user/index.vue
+++ b/src/components/chat/user/index.vue
@@ -57,8 +57,10 @@
 </template>
 
 <script setup lang="ts" name="UserMsg">
+import { ElMessage } from 'element-plus';
 import { AnswerState, answerTypeMapCom, roleImageMap, type ChatMessage } from '../model/types';
 import { isSharePage } from '/@/stores/chatRoom';
+import { onClickOutside, useClipboard } from '@vueuse/core';
 
 const emit = defineEmits<{
 	(event: 'copyMsg', msgObj: ChatMessage): void;
@@ -71,10 +73,13 @@
 		type: Object,
 	},
 });
+const { copy } = useClipboard();
 
 //鐢ㄦ埛澶嶅埗闂
 const copyUserClick = (item) => {
-	emit('copyMsg', item);
+	const text = item.content.values;
+	copy(text);
+	ElMessage.success('澶嶅埗鎴愬姛');
 };
 //鐢ㄦ埛闂璁剧疆涓哄父鐢ㄨ
 const setCommonQuestionClick = (item) => {

--
Gitblit v1.9.3