From e938a76be49d203d5aa576caa2afbbb20bd65e66 Mon Sep 17 00:00:00 2001 From: wizardchen Date: Thu, 16 Jul 2026 21:57:05 +0800 Subject: [PATCH] feat(chat): enhance follow-up suggestions rendering logic Implement a mechanism to ensure follow-up suggestions are only displayed after the answer is fully rendered. Introduce an event for rendering completion and update related components to handle this new logic. Update tests to verify the correct behavior of follow-up suggestions based on rendering state. --- .../views/chat/components/AgentStreamDisplay.vue | 7 ++++++- .../components/answerToolbarCompletion.test.mjs | 13 +++++++++++++ frontend/src/views/chat/components/botmsg.vue | 16 +++++++++++++--- frontend/src/views/chat/index.vue | 9 +++++++-- 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/frontend/src/views/chat/components/AgentStreamDisplay.vue b/frontend/src/views/chat/components/AgentStreamDisplay.vue index 5178eadd..8cfe8389 100644 --- a/frontend/src/views/chat/components/AgentStreamDisplay.vue +++ b/frontend/src/views/chat/components/AgentStreamDisplay.vue @@ -782,6 +782,10 @@ const props = defineProps<{ followUpLoading?: boolean; }>(); +const emit = defineEmits<{ + (event: 'render-complete-change', ready: boolean): void; +}>(); + const embedAuthProps = computed(() => ({ embeddedMode: props.embeddedMode, embedChannelId: props.embedChannelId, @@ -1257,6 +1261,7 @@ const answerFullyRendered = computed( () => isConversationDone.value && typedAnswer.value.length >= activeAnswerMarkdown.value.length, ); watch(answerFullyRendered, (ready) => { + emit('render-complete-change', ready); if (!ready) return; // Clear before this reactive update renders, so a source that returned 404 // mid-stream gets one real final-attempt node instead of remaining @@ -1265,7 +1270,7 @@ watch(answerFullyRendered, (ready) => { nextTick(async () => { await hydrateProtectedFileImages(rootElement.value); }); -}); +}, { immediate: true }); // Whether any currently visible step is actively pending (a running tool, or a // blocking approval/OAuth prompt). A pending step shimmers on its own, so we diff --git a/frontend/src/views/chat/components/answerToolbarCompletion.test.mjs b/frontend/src/views/chat/components/answerToolbarCompletion.test.mjs index 7168fdb5..b901e54e 100644 --- a/frontend/src/views/chat/components/answerToolbarCompletion.test.mjs +++ b/frontend/src/views/chat/components/answerToolbarCompletion.test.mjs @@ -35,3 +35,16 @@ test('follow-up loading is shown compactly inside both answer toolbars', () => { assert.match(sharedStyles, /background-clip: text/) assert.match(sharedStyles, /follow-up-toolbar-loading-leave-to/) }) + +test('follow-up suggestions wait until the answer is fully rendered', () => { + assert.match( + chatView, + /@render-complete-change="\(ready\) => handleAnswerRenderComplete\(session, ready\)"/, + ) + assert.match( + chatView, + / + :user-query="userQuery" :rag-mode="true" :follow-up-loading="followUpLoading" + @render-complete-change="emit('render-complete-change', $event)" /> @@ -117,7 +119,7 @@ const mentionTagIcon = (item) => { return 'file'; }; -const emit = defineEmits(['scroll-bottom']) +const emit = defineEmits(['scroll-bottom', 'render-complete-change']) const { t } = useI18n() const uiStore = useUIStore(); let parentMd = ref() @@ -205,6 +207,14 @@ const answerFullyRendered = computed(() => Boolean(props.session?.is_completed) && typedAnswer.value.length >= answerText.value.length ); +watch( + answerFullyRendered, + (ready) => { + if (!props.session?.isAgentMode) emit('render-complete-change', ready); + }, + { immediate: true }, +); + // 单次渲染整个 Markdown 内容(替代 token-by-token,修复 KaTeX 公式在 streaming 时闪烁消失的问题) const renderedHTML = computed(() => { const text = typedAnswer.value; diff --git a/frontend/src/views/chat/index.vue b/frontend/src/views/chat/index.vue index d0fedf9c..c1ac2845 100644 --- a/frontend/src/views/chat/index.vue +++ b/frontend/src/views/chat/index.vue @@ -85,9 +85,10 @@ + :follow-up-loading="Boolean(session.suggestionLoading && !session.suggestionSet?.questions?.length)" + @render-complete-change="(ready) => handleAnswerRenderComplete(session, ready)"> - { const resolveAssistantMessageId = (message) => message?.id || message?.assistant_message_id; +const handleAnswerRenderComplete = (message, ready) => { + message.answerFullyRendered = Boolean(ready); +}; + const loadFollowUpSuggestions = async (message, ensure = false, regenerate = false) => { const messageId = resolveAssistantMessageId(message); const targetSessionId = session_id.value;