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.
This commit is contained in:
wizardchen
2026-07-16 21:57:05 +08:00
committed by lyingbug
parent b862c2e45d
commit e938a76be4
4 changed files with 39 additions and 6 deletions
@@ -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 <img> 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
@@ -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,
/<FollowUpSuggestions v-if="session\.answerFullyRendered && !session\.suggestionsDismissed"/,
)
assert.match(botMessage, /emit\('render-complete-change', ready\)/)
assert.match(agentStream, /emit\('render-complete-change', ready\)/)
})
+13 -3
View File
@@ -17,12 +17,14 @@
<div v-if="session.isRagMode" class="rag-answer-stack">
<RagPipelineProgress :session="session" :embedded-mode="embeddedMode" />
<AgentStreamDisplay v-if="session.isAgentMode" :session="session" :session-id="sessionId"
:user-query="userQuery" :rag-mode="true" :follow-up-loading="followUpLoading" />
:user-query="userQuery" :rag-mode="true" :follow-up-loading="followUpLoading"
@render-complete-change="emit('render-complete-change', $event)" />
</div>
<template v-else>
<docInfo v-if="session.knowledge_references?.length" :session="session"></docInfo>
<AgentStreamDisplay :session="session" :session-id="sessionId" :user-query="userQuery"
v-if="session.isAgentMode" :follow-up-loading="followUpLoading" />
v-if="session.isAgentMode" :follow-up-loading="followUpLoading"
@render-complete-change="emit('render-complete-change', $event)" />
</template>
<deepThink :deepSession="session" v-if="session.showThink && !session.isAgentMode"></deepThink>
</div>
@@ -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;
+7 -2
View File
@@ -85,9 +85,10 @@
<botmsg :content="session.content" :session="session" :session-id="session_id"
:user-query="getUserQuery(index)" @scroll-bottom="scrollToBottom"
:isFirstEnter="isFirstEnter" :embeddedMode="embeddedMode"
:follow-up-loading="Boolean(session.suggestionLoading && !session.suggestionSet?.questions?.length)">
:follow-up-loading="Boolean(session.suggestionLoading && !session.suggestionSet?.questions?.length)"
@render-complete-change="(ready) => handleAnswerRenderComplete(session, ready)">
</botmsg>
<FollowUpSuggestions v-if="!session.suggestionsDismissed"
<FollowUpSuggestions v-if="session.answerFullyRendered && !session.suggestionsDismissed"
:suggestion-set="session.suggestionSet"
:loading="session.suggestionLoading"
:allow-regenerate="session.suggestionSet?.allow_regenerate"
@@ -331,6 +332,10 @@ const handleSuggestedQuestionClick = (question) => {
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;