feat: 增强聊天组件功能,优化消息展示和滚动逻辑

- 在 AgentChatComponent.vue 中新增对话最后一个消息使用的模型显示。
- 添加生成中的加载状态指示,提升用户体验。
- 优化滚动控制逻辑,确保在消息更新时自动滚动到底部。
- 在 AgentMessageComponent.vue 中更新消息解析逻辑,处理推理内容。
- 在 RefsComponent.vue 中增强模型信息的显示和交互功能。
This commit is contained in:
Wenjie Zhang 2025-06-24 00:15:51 +08:00
parent 17ec5c5f77
commit bb4793e785
3 changed files with 181 additions and 17 deletions

View File

@ -66,6 +66,13 @@
@retry="retryMessage(message)" @retry="retryMessage(message)"
> >
</AgentMessageComponent> </AgentMessageComponent>
<!-- 显示对话最后一个消息使用的模型 -->
<RefsComponent
v-if="getLastMessage(conv)"
:message="getLastMessage(conv)"
:show-refs="['model', 'copy']"
:is-latest-message="false"
/>
</div> </div>
<div class="conv-box" v-if="onGoingConv.messages.length > 0"> <div class="conv-box" v-if="onGoingConv.messages.length > 0">
<AgentMessageComponent <AgentMessageComponent
@ -79,6 +86,18 @@
> >
</AgentMessageComponent> </AgentMessageComponent>
</div> </div>
<!-- 生成中的加载状态 -->
<div class="generating-status" v-if="state.isProcessingRequest && !state.waitingServerResponse">
<div class="generating-indicator">
<div class="loading-dots">
<div></div>
<div></div>
<div></div>
</div>
<span class="generating-text">正在生成回复...</span>
</div>
</div>
</div> </div>
<div class="bottom"> <div class="bottom">
<div class="message-input-wrapper"> <div class="message-input-wrapper">
@ -111,6 +130,7 @@ import { message } from 'ant-design-vue';
import MessageInputComponent from '@/components/MessageInputComponent.vue' import MessageInputComponent from '@/components/MessageInputComponent.vue'
import AgentMessageComponent from '@/components/AgentMessageComponent.vue' import AgentMessageComponent from '@/components/AgentMessageComponent.vue'
import ChatSidebarComponent from '@/components/ChatSidebarComponent.vue' import ChatSidebarComponent from '@/components/ChatSidebarComponent.vue'
import RefsComponent from '@/components/RefsComponent.vue'
import { chatApi, threadApi } from '@/apis/auth_api' import { chatApi, threadApi } from '@/apis/auth_api'
import { PanelLeftOpen, MessageSquarePlus } from 'lucide-vue-next'; import { PanelLeftOpen, MessageSquarePlus } from 'lucide-vue-next';
@ -150,7 +170,12 @@ const isSmallContainer = computed(() => containerWidth.value <= 520);
const isMediumContainer = computed(() => containerWidth.value <= 768); const isMediumContainer = computed(() => containerWidth.value <= 768);
let resizeObserver = null; let resizeObserver = null;
// //
const shouldAutoScroll = ref(true); //
const isUserScrolling = ref(false); //
let scrollTimer = null;
//
onMounted(() => { onMounted(() => {
// //
nextTick(() => { nextTick(() => {
@ -166,6 +191,12 @@ onMounted(() => {
resizeObserver.observe(chatContainerRef.value); resizeObserver.observe(chatContainerRef.value);
} }
//
const chatContainer = document.querySelector('.chat');
if (chatContainer) {
chatContainer.addEventListener('scroll', handleScroll, { passive: true });
}
}); });
// //
@ -178,6 +209,16 @@ onUnmounted(() => {
if (resizeObserver) { if (resizeObserver) {
resizeObserver.disconnect(); resizeObserver.disconnect();
} }
//
const chatContainer = document.querySelector('.chat');
if (chatContainer) {
chatContainer.removeEventListener('scroll', handleScroll);
}
if (scrollTimer) {
clearTimeout(scrollTimer);
}
}); });
const showMsgRefs = (msg) => { const showMsgRefs = (msg) => {
@ -187,6 +228,21 @@ const showMsgRefs = (msg) => {
return false return false
} }
//
const getLastMessage = (conv) => {
if (!conv || !conv.messages || conv.messages.length === 0) {
return null;
}
// AI
for (let i = conv.messages.length - 1; i >= 0; i--) {
const message = conv.messages[i];
if (message.type === 'ai') {
return message;
}
}
return null;
}
// DOM // DOM
const messagesContainer = ref(null); const messagesContainer = ref(null);
@ -375,6 +431,8 @@ const sendMessageToServer = async (text) => {
renameChat({'chatId': currentChatId.value, 'title': text}) renameChat({'chatId': currentChatId.value, 'title': text})
} }
//
shouldAutoScroll.value = true;
state.isProcessingRequest = true; state.isProcessingRequest = true;
await scrollToBottom(); await scrollToBottom();
@ -480,8 +538,11 @@ const processResponseChunk = async (data) => {
} }
onGoingConv.msgChunks[data.msg.id].push(data.msg) onGoingConv.msgChunks[data.msg.id].push(data.msg)
} }
} else if (data.status === 'error') {
console.error("流式处理出错:", data.message);
message.error(data.message);
} else if (data.status === 'finished') { } else if (data.status === 'finished') {
// await getAgentHistory(); await getAgentHistory();
} }
// await scrollToBottom(); // await scrollToBottom();
}; };
@ -561,6 +622,11 @@ const getAgentHistory = async () => {
// //
onGoingConv.msgChunks = {}; onGoingConv.msgChunks = {};
convs.value = convertServerHistoryToMessages(response.history); convs.value = convertServerHistoryToMessages(response.history);
//
shouldAutoScroll.value = true;
await nextTick();
await scrollToBottom();
} else { } else {
message.warning('未找到历史记录或格式不正确'); message.warning('未找到历史记录或格式不正确');
} }
@ -652,20 +718,49 @@ onMounted(() => {
}); });
// //
// watch(convs, () => { watch([convs, () => onGoingConv.messages], () => {
// scrollToBottom(); scrollToBottom();
// }, { deep: true }); }, { deep: true });
// ==================== ==================== // ==================== ====================
// TODO: //
const isAtBottom = () => {
const container = document.querySelector('.chat');
if (!container) return false;
const threshold = 100; // 100px
const isBottom = container.scrollHeight - container.scrollTop - container.clientHeight <= threshold;
return isBottom;
};
//
const handleScroll = () => {
if (scrollTimer) {
clearTimeout(scrollTimer);
}
//
isUserScrolling.value = true;
//
const atBottom = isAtBottom();
shouldAutoScroll.value = atBottom;
//
scrollTimer = setTimeout(() => {
isUserScrolling.value = false;
}, 150);
};
//
const scrollToBottom = async () => { const scrollToBottom = async () => {
await nextTick(); await nextTick();
if (!messagesContainer.value) return;
// //
const containerBox = messagesContainer.value; if (!shouldAutoScroll.value) return;
const container = document.querySelector('.chat'); const container = document.querySelector('.chat');
if (!container) return; if (!container) return;
@ -673,9 +768,21 @@ const scrollToBottom = async () => {
// //
container.scrollTo(scrollOptions); container.scrollTo(scrollOptions);
setTimeout(() => container.scrollTo(scrollOptions), 50); setTimeout(() => {
setTimeout(() => container.scrollTo(scrollOptions), 150); if (shouldAutoScroll.value) {
setTimeout(() => container.scrollTo({ top: container.scrollHeight, behavior: 'auto' }), 300); container.scrollTo(scrollOptions);
}
}, 50);
setTimeout(() => {
if (shouldAutoScroll.value) {
container.scrollTo(scrollOptions);
}
}, 150);
setTimeout(() => {
if (shouldAutoScroll.value) {
container.scrollTo({ top: container.scrollHeight, behavior: 'auto' });
}
}, 300);
}; };
@ -713,8 +820,11 @@ const mergeMessageChunk = (chunks) => {
} }
// chunk // chunk
const result = JSON.parse(JSON.stringify(chunks[0])); const result = JSON.parse(JSON.stringify(chunks[0]));
console.debug("result", toRaw(result))
result.content = result.content || ''; result.content = result.content || '';
result.additional_kwargs = result.additional_kwargs || {};
result.additional_kwargs.reasoning_content = result.additional_kwargs?.reasoning_content || '';
// chunks // chunks
for (let i = 1; i < chunks.length; i++) { for (let i = 1; i < chunks.length; i++) {
@ -722,6 +832,7 @@ const mergeMessageChunk = (chunks) => {
// content // content
result.content += chunk.content || ''; result.content += chunk.content || '';
result.additional_kwargs.reasoning_content += chunk.additional_kwargs?.reasoning_content || '';
// chunk key, value, result[key] result // chunk key, value, result[key] result
for (const key in chunk) { for (const key in chunk) {
@ -1029,6 +1140,28 @@ const mergeMessageChunk = (chunks) => {
animation-delay: -0.16s; animation-delay: -0.16s;
} }
.generating-status {
display: flex;
justify-content: flex-start;
padding: 0.8rem 0;
animation: fadeInUp 0.3s ease-out;
}
.generating-indicator {
display: flex;
align-items: center;
padding: 0.75rem 1rem;
background: var(--gray-100);
border-radius: 12px;
border: 1px solid var(--gray-200);
.generating-text {
margin-left: 12px;
color: var(--gray-700);
font-size: 14px;
}
}
.toggle-sidebar { .toggle-sidebar {
cursor: pointer; cursor: pointer;

View File

@ -154,6 +154,7 @@ const parsedMessage = computed(() => {
} }
} }
} }
message.reasoning_content = message.reasoning_content || message.additional_kwargs?.reasoning_content || '';
return message; return message;
}); });

View File

@ -1,10 +1,10 @@
<template> <template>
<div class="refs" v-if="showRefs"> <div class="refs" v-if="showRefs">
<div class="tags"> <div class="tags">
<!-- <span class="item btn" @click="likeThisResponse(msg)"><LikeOutlined /></span> --> <span class="item btn" @click="likeThisResponse(msg)"><LikeOutlined /></span>
<!-- <span class="item btn" @click="dislikeThisResponse(msg)"><DislikeOutlined /></span> --> <span class="item btn" @click="dislikeThisResponse(msg)"><DislikeOutlined /></span>
<span v-if="msg.meta?.server_model_name" class="item" @click="console.log(msg)"> <span v-if="showKey('model') && getModelName(msg)" class="item" @click="console.log(msg)">
<BulbOutlined /> {{ msg.meta.server_model_name }} <BulbOutlined /> {{ getModelName(msg) }}
</span> </span>
<span <span
v-if="showKey('copy')" v-if="showKey('copy')"
@ -48,6 +48,8 @@ import {
DeploymentUnitOutlined, DeploymentUnitOutlined,
BulbOutlined, BulbOutlined,
ReloadOutlined, ReloadOutlined,
LikeOutlined,
DislikeOutlined,
} from '@ant-design/icons-vue' } from '@ant-design/icons-vue'
const emit = defineEmits(['retry', 'openRefs']); const emit = defineEmits(['retry', 'openRefs']);
@ -91,7 +93,14 @@ const copyText = async (text) => {
} }
} }
const showRefs = computed(() => (msg.value.role=='received' || msg.value.role=='assistant') && msg.value.status=='finished') const showRefs = computed(() => {
//
if (props.showRefs && Array.isArray(props.showRefs) && props.showRefs.includes('model')) {
return true;
}
//
return (msg.value.role=='received' || msg.value.role=='assistant') && msg.value.status=='finished';
})
// refs // refs
const openGlobalRefs = (type) => { const openGlobalRefs = (type) => {
@ -117,6 +126,27 @@ const hasKnowledgeBaseData = (msg) => {
const regenerateMessage = () => { const regenerateMessage = () => {
emit('retry') emit('retry')
} }
//
const getModelName = (msg) => {
// response_metadata.model_name
if (msg.response_metadata?.model_name) {
return msg.response_metadata.model_name;
}
// meta.server_model_name
if (msg.meta?.server_model_name) {
return msg.meta.server_model_name;
}
return null;
}
const likeThisResponse = (msg) => {
console.log(msg)
}
const dislikeThisResponse = (msg) => {
console.log(msg)
}
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>