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)" />
+ v-if="session.isAgentMode" :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;