From ca959b5c2f78af7a8b612bf40fef360b919a0444 Mon Sep 17 00:00:00 2001 From: lyingbug <[email> Date: Tue, 4 Aug 2026 04:34:58 +0000 Subject: [PATCH 1/2] fix(frontend): cover attachment-only turns and stalled streams in the RAG wait row The wait row only appeared once a completed knowledge_search step existed, so attachment-only turns (attachment_parsing / image_analysis with no KB hit) still showed nothing between the last step and the first answer token. Model waiting is now a three-state kind: 'model' after retrieval finished, the neutral 'preparing' row otherwise. A dropped SSE connection never sets is_completed (the stream layer only raises a toast), so the row used to promise an answer forever. It now stops claiming progress after RAG_WAIT_STALL_DELAY_MS and drops the shimmer. Also share RAG_RETRIEVAL_TOOL_NAMES instead of repeating the knowledge_search / search_knowledge alias check, name the reveal delay, and move the reveal/stall timers into a controller with an injectable scheduler so the timing is covered by tests instead of by source-matching alone. Co-authored-by: lyingbug --- frontend/src/i18n/embed.ts | 4 + frontend/src/i18n/locales/en-US.ts | 1 + frontend/src/i18n/locales/ko-KR.ts | 1 + frontend/src/i18n/locales/ru-RU.ts | 1 + frontend/src/i18n/locales/zh-CN.ts | 1 + frontend/src/utils/rag-pipeline-history.ts | 3 + frontend/src/utils/rag-pipeline-state.test.ts | 161 +++++++++++++++--- frontend/src/utils/rag-pipeline-state.ts | 122 +++++++++++-- .../RagPipelineProgress.style.test.mjs | 8 +- .../chat/components/RagPipelineProgress.vue | 66 ++++--- 10 files changed, 293 insertions(+), 75 deletions(-) diff --git a/frontend/src/i18n/embed.ts b/frontend/src/i18n/embed.ts index 4ffc3943b48..dd9c1b881f0 100644 --- a/frontend/src/i18n/embed.ts +++ b/frontend/src/i18n/embed.ts @@ -108,6 +108,7 @@ const messages = { "thinkingAlt": "正在思考", "preparingAnswer": "正在准备回答…", "connectingModelAndGeneratingAnswer": "正在连接模型并生成回答…", + "modelStillResponding": "模型响应较慢,仍在等待…", "deepThoughtCompleted": "已深度思考", "deepThoughtAlt": "深度思考完成", "referencesTitle": "参考了{count}个相关内容", @@ -600,6 +601,7 @@ const messages = { "thinkingAlt": "Thinking in progress", "preparingAnswer": "Preparing an answer…", "connectingModelAndGeneratingAnswer": "Connecting to the model and generating an answer…", + "modelStillResponding": "The model is taking longer than usual, still waiting…", "deepThoughtCompleted": "Deep thinking completed", "deepThoughtAlt": "Deep thinking finished", "referencesTitle": "Referenced {count} related item(s)", @@ -1054,6 +1056,7 @@ const koEmbedPublish = { thinkingAlt: '생각 중', preparingAnswer: '답변을 준비하고 있습니다…', connectingModelAndGeneratingAnswer: '모델에 연결하여 답변을 생성하고 있습니다…', + modelStillResponding: '모델 응답이 평소보다 오래 걸리고 있습니다. 계속 기다리는 중…', refreshSuggestedQuestions: '다른 질문', imageTooMany: '이미지는 최대 5장까지 업로드할 수 있습니다', imageTypeSizeError: 'JPG/PNG/GIF/WEBP만 지원하며, 각 파일은 10MB 이하여야 합니다', @@ -1144,6 +1147,7 @@ const ruEmbedPublish = { thinkingAlt: 'Обдумывание...', preparingAnswer: 'Подготовка ответа…', connectingModelAndGeneratingAnswer: 'Подключение к модели и создание ответа…', + modelStillResponding: 'Модель отвечает дольше обычного, продолжаем ждать…', refreshSuggestedQuestions: 'Ещё', imageTooMany: 'Можно загрузить не более 5 изображений', imageTypeSizeError: 'Поддерживаются только JPG/PNG/GIF/WEBP, каждый файл до 10 МБ', diff --git a/frontend/src/i18n/locales/en-US.ts b/frontend/src/i18n/locales/en-US.ts index 00f1720b3bc..05f5796e8e4 100755 --- a/frontend/src/i18n/locales/en-US.ts +++ b/frontend/src/i18n/locales/en-US.ts @@ -2560,6 +2560,7 @@ export default { thinkingAlt: 'Thinking in progress', preparingAnswer: 'Preparing an answer…', connectingModelAndGeneratingAnswer: 'Connecting to the model and generating an answer…', + modelStillResponding: 'The model is taking longer than usual, still waiting…', deepThoughtCompleted: 'Deep thinking completed', deepThoughtAlt: 'Deep thinking finished', referencesTitle: 'Referenced {count} related item(s)', diff --git a/frontend/src/i18n/locales/ko-KR.ts b/frontend/src/i18n/locales/ko-KR.ts index 37ed3ef65e2..e7e6414ca12 100755 --- a/frontend/src/i18n/locales/ko-KR.ts +++ b/frontend/src/i18n/locales/ko-KR.ts @@ -3048,6 +3048,7 @@ export default { thinkingAlt: '생각 중', preparingAnswer: '답변을 준비하고 있습니다…', connectingModelAndGeneratingAnswer: '모델에 연결하여 답변을 생성하고 있습니다…', + modelStillResponding: '모델 응답이 평소보다 오래 걸리고 있습니다. 계속 기다리는 중…', deepThoughtCompleted: '심층 분석 완료', deepThoughtAlt: '심층 분석 완료', referencesTitle: '{count}개의 관련 내용 참조', diff --git a/frontend/src/i18n/locales/ru-RU.ts b/frontend/src/i18n/locales/ru-RU.ts index 3d1f5f7a034..533caa264ee 100755 --- a/frontend/src/i18n/locales/ru-RU.ts +++ b/frontend/src/i18n/locales/ru-RU.ts @@ -3048,6 +3048,7 @@ export default { thinkingAlt: 'Обдумывание...', preparingAnswer: 'Подготовка ответа…', connectingModelAndGeneratingAnswer: 'Подключение к модели и создание ответа…', + modelStillResponding: 'Модель отвечает дольше обычного, продолжаем ждать…', deepThoughtCompleted: 'Глубокий анализ завершён', deepThoughtAlt: 'Глубокий анализ', referencesTitle: 'Использовано {count} связанного материала', diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index 285aa678a75..aab24a9d5fe 100755 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -3048,6 +3048,7 @@ export default { thinkingAlt: '正在思考', preparingAnswer: '正在准备回答…', connectingModelAndGeneratingAnswer: '正在连接模型并生成回答…', + modelStillResponding: '模型响应较慢,仍在等待…', deepThoughtCompleted: '已深度思考', deepThoughtAlt: '深度思考完成', referencesTitle: '参考了{count}个相关内容', diff --git a/frontend/src/utils/rag-pipeline-history.ts b/frontend/src/utils/rag-pipeline-history.ts index 70f841594d6..b8bd2a1860c 100644 --- a/frontend/src/utils/rag-pipeline-history.ts +++ b/frontend/src/utils/rag-pipeline-history.ts @@ -1,5 +1,8 @@ export const RAG_PIPELINE_TOOL_NAMES = new Set(['query_understand', 'knowledge_search']) +/** Retrieval tools that can produce citations. `search_knowledge` is the legacy alias. */ +export const RAG_RETRIEVAL_TOOL_NAMES = new Set(['knowledge_search', 'search_knowledge']) + /** Tools rendered on the quick-answer timeline (includes pre-RAG attachment prep). */ export const RAG_TIMELINE_TOOL_NAMES = new Set([ ...RAG_PIPELINE_TOOL_NAMES, diff --git a/frontend/src/utils/rag-pipeline-state.test.ts b/frontend/src/utils/rag-pipeline-state.test.ts index 3f44055b299..6705c09cea0 100644 --- a/frontend/src/utils/rag-pipeline-state.test.ts +++ b/frontend/src/utils/rag-pipeline-state.test.ts @@ -1,6 +1,13 @@ import assert from 'node:assert/strict' import test from 'node:test' -import { shouldShowRagModelAnswerWait } from './rag-pipeline-state.ts' +import { + RAG_WAIT_REVEAL_DELAY_MS, + RAG_WAIT_STALL_DELAY_MS, + createRagWaitController, + getRagPipelineWaitKind, + type RagWaitScheduler, + type RagWaitView, +} from './rag-pipeline-state.ts' const completedRetrieval = { isCompleted: false, @@ -11,42 +18,146 @@ const completedRetrieval = { hasCompletedRetrievalStep: true, } -test('shows the model-answer wait state after the RAG pipeline finishes', () => { - assert.equal(shouldShowRagModelAnswerWait(completedRetrieval), true) +test('waits on the model once retrieval finished', () => { + assert.equal(getRagPipelineWaitKind(completedRetrieval), 'model') }) -test('does not show the model-answer wait state while a pipeline step is pending', () => { - assert.equal(shouldShowRagModelAnswerWait({ +test('waits while a pipeline step is still pending', () => { + assert.equal(getRagPipelineWaitKind({ ...completedRetrieval, allStepsDone: false, - }), false) + }), 'none') }) -test('does not show the model-answer wait state before pipeline events arrive', () => { - assert.equal(shouldShowRagModelAnswerWait({ +test('waits for nothing before pipeline events arrive', () => { + assert.equal(getRagPipelineWaitKind({ ...completedRetrieval, stepCount: 0, - }), false) + }), 'none') }) -test('does not mistake a completed pre-retrieval step for the model-answer wait', () => { - assert.equal(shouldShowRagModelAnswerWait({ +test('falls back to the neutral preparing state when no retrieval step ran', () => { + assert.equal(getRagPipelineWaitKind({ ...completedRetrieval, hasCompletedRetrievalStep: false, - }), false) + }), 'preparing') }) -test('hides the model-answer wait state when thinking, answer, or completion arrives', () => { - assert.equal(shouldShowRagModelAnswerWait({ - ...completedRetrieval, - hasThinkingEvent: true, - }), false) - assert.equal(shouldShowRagModelAnswerWait({ - ...completedRetrieval, - hasAnswer: true, - }), false) - assert.equal(shouldShowRagModelAnswerWait({ - ...completedRetrieval, - isCompleted: true, - }), false) +test('stops waiting when thinking, answer, or completion arrives', () => { + assert.equal(getRagPipelineWaitKind({ ...completedRetrieval, hasThinkingEvent: true }), 'none') + assert.equal(getRagPipelineWaitKind({ ...completedRetrieval, hasAnswer: true }), 'none') + assert.equal(getRagPipelineWaitKind({ ...completedRetrieval, isCompleted: true }), 'none') +}) + +function createFakeScheduler() { + let now = 0 + let nextHandle = 1 + const jobs = new Map void }>() + + const scheduler: RagWaitScheduler = { + setTimeout: (callback, ms) => { + const handle = nextHandle++ + jobs.set(handle, { runAt: now + ms, callback }) + return handle + }, + clearTimeout: (handle) => { + jobs.delete(handle as number) + }, + } + + const advance = (ms: number) => { + now += ms + for (const [handle, job] of [...jobs].sort((a, b) => a[1].runAt - b[1].runAt)) { + if (job.runAt > now) continue + jobs.delete(handle) + job.callback() + } + } + + return { scheduler, advance, pendingCount: () => jobs.size } +} + +function createHarness() { + const views: RagWaitView[] = [] + const { scheduler, advance, pendingCount } = createFakeScheduler() + const controller = createRagWaitController((view) => views.push(view), scheduler) + return { controller, views, advance, pendingCount } +} + +test('keeps the wait row hidden while the model answers quickly', () => { + const { controller, views, advance } = createHarness() + + controller.update('model') + advance(RAG_WAIT_REVEAL_DELAY_MS - 1) + controller.update('none') + advance(RAG_WAIT_STALL_DELAY_MS) + + assert.deepEqual(views, []) +}) + +test('reveals the wait row once the model stays quiet past the delay', () => { + const { controller, views, advance } = createHarness() + + controller.update('model') + advance(RAG_WAIT_REVEAL_DELAY_MS) + + assert.deepEqual(views, [{ kind: 'model', stalled: false }]) + + controller.update('none') + assert.deepEqual(views.at(-1), { kind: 'none', stalled: false }) +}) + +test('marks the wait row stalled when no answer ever arrives', () => { + const { controller, views, advance } = createHarness() + + controller.update('model') + advance(RAG_WAIT_REVEAL_DELAY_MS) + advance(RAG_WAIT_STALL_DELAY_MS - 1) + + assert.deepEqual(views, [{ kind: 'model', stalled: false }]) + + advance(1) + + assert.deepEqual(views.at(-1), { kind: 'model', stalled: true }) +}) + +test('swaps the label in place instead of re-running the reveal delay', () => { + const { controller, views, advance } = createHarness() + + controller.update('preparing') + advance(RAG_WAIT_REVEAL_DELAY_MS) + controller.update('model') + + assert.deepEqual(views, [ + { kind: 'preparing', stalled: false }, + { kind: 'model', stalled: false }, + ]) +}) + +test('gives each phase a fresh stall budget', () => { + const { controller, views, advance } = createHarness() + + controller.update('preparing') + advance(RAG_WAIT_REVEAL_DELAY_MS) + advance(RAG_WAIT_STALL_DELAY_MS - 1) + controller.update('model') + advance(RAG_WAIT_STALL_DELAY_MS - 1) + + assert.deepEqual(views.at(-1), { kind: 'model', stalled: false }) + + advance(1) + + assert.deepEqual(views.at(-1), { kind: 'model', stalled: true }) +}) + +test('drops pending timers on dispose', () => { + const { controller, views, advance, pendingCount } = createHarness() + + controller.update('model') + controller.dispose() + + assert.equal(pendingCount(), 0) + + advance(RAG_WAIT_STALL_DELAY_MS) + assert.deepEqual(views, []) }) diff --git a/frontend/src/utils/rag-pipeline-state.ts b/frontend/src/utils/rag-pipeline-state.ts index bd063f0b41f..3a530aef0b0 100644 --- a/frontend/src/utils/rag-pipeline-state.ts +++ b/frontend/src/utils/rag-pipeline-state.ts @@ -1,4 +1,16 @@ -export interface RagModelAnswerWaitState { +/** How long the wait row stays hidden so a fast model answer never flashes it. */ +export const RAG_WAIT_REVEAL_DELAY_MS = 250 + +/** + * How long the wait row keeps claiming progress. A dropped SSE connection never + * sets `is_completed` (the stream layer only raises a toast), so without this cap + * the row would promise an answer forever. + */ +export const RAG_WAIT_STALL_DELAY_MS = 60_000 + +export type RagWaitKind = 'none' | 'preparing' | 'model' + +export interface RagPipelineWaitInput { isCompleted: boolean hasAnswer: boolean hasThinkingEvent: boolean @@ -8,16 +20,102 @@ export interface RagModelAnswerWaitState { } /** - * Show a model-answer wait state only in the quiet gap after every visible RAG - * pipeline step has finished and before the model emits thinking or answer text. + * Describe the quiet gap after every visible RAG pipeline step has finished and + * before the model emits thinking or answer text. + * + * `model` is only claimed once retrieval actually finished; turns that never run + * a retrieval step (attachment-only Q&A) still get the neutral `preparing` row + * rather than no feedback at all. + */ +export function getRagPipelineWaitKind(state: RagPipelineWaitInput): RagWaitKind { + if (state.isCompleted || state.hasAnswer || state.hasThinkingEvent) return 'none' + if (state.stepCount === 0 || !state.allStepsDone) return 'none' + return state.hasCompletedRetrievalStep ? 'model' : 'preparing' +} + +export interface RagWaitView { + kind: RagWaitKind + stalled: boolean +} + +export interface RagWaitScheduler { + setTimeout: (callback: () => void, ms: number) => unknown + clearTimeout: (handle: unknown) => void +} + +export interface RagWaitController { + update: (kind: RagWaitKind) => void + dispose: () => void +} + +const defaultScheduler: RagWaitScheduler = { + setTimeout: (callback, ms) => setTimeout(callback, ms), + clearTimeout: (handle) => clearTimeout(handle as ReturnType), +} + +/** + * Turn the raw wait kind into what the timeline renders: delayed reveal, in-place + * label swaps once visible, and a stalled state when the answer never arrives. */ -export function shouldShowRagModelAnswerWait(state: RagModelAnswerWaitState): boolean { - return ( - !state.isCompleted && - !state.hasAnswer && - !state.hasThinkingEvent && - state.stepCount > 0 && - state.allStepsDone && - state.hasCompletedRetrievalStep - ) +export function createRagWaitController( + onChange: (view: RagWaitView) => void, + scheduler: RagWaitScheduler = defaultScheduler, +): RagWaitController { + let target: RagWaitKind = 'none' + let view: RagWaitView = { kind: 'none', stalled: false } + let revealHandle: unknown + let stallHandle: unknown + + const cancelTimers = () => { + if (revealHandle !== undefined) { + scheduler.clearTimeout(revealHandle) + revealHandle = undefined + } + if (stallHandle !== undefined) { + scheduler.clearTimeout(stallHandle) + stallHandle = undefined + } + } + + const emit = (next: RagWaitView) => { + if (next.kind === view.kind && next.stalled === view.stalled) return + view = next + onChange(view) + } + + const armStall = () => { + stallHandle = scheduler.setTimeout(() => { + stallHandle = undefined + emit({ kind: target, stalled: true }) + }, RAG_WAIT_STALL_DELAY_MS) + } + + return { + update(kind) { + if (kind === target) return + target = kind + cancelTimers() + + if (kind === 'none') { + emit({ kind: 'none', stalled: false }) + return + } + + if (view.kind !== 'none') { + emit({ kind, stalled: false }) + armStall() + return + } + + revealHandle = scheduler.setTimeout(() => { + revealHandle = undefined + emit({ kind: target, stalled: false }) + armStall() + }, RAG_WAIT_REVEAL_DELAY_MS) + }, + dispose() { + cancelTimers() + target = 'none' + }, + } } diff --git a/frontend/src/views/chat/components/RagPipelineProgress.style.test.mjs b/frontend/src/views/chat/components/RagPipelineProgress.style.test.mjs index 712a255d686..de591cb7756 100644 --- a/frontend/src/views/chat/components/RagPipelineProgress.style.test.mjs +++ b/frontend/src/views/chat/components/RagPipelineProgress.style.test.mjs @@ -59,10 +59,14 @@ test('rag pipeline uses a native pending step and lets the thinking title shimme }) test('rag pipeline shows a pending model-answer step after retrieval completes', () => { - assert.match(source, /showModelAnswerWait/) - assert.match(source, /shouldShowRagModelAnswerWait/) + assert.match(source, /showWaitStep/) + assert.match(source, /getRagPipelineWaitKind/) + assert.match(source, /createRagWaitController/) assert.match(source, /t\('chat\.connectingModelAndGeneratingAnswer'\)/) + assert.match(source, /t\('chat\.modelStillResponding'\)/) assert.match(source, /rag-model-wait-step/) + assert.match(source, /'action-pending': !waitStepStalled/) + assert.match(source, /waitController\.dispose\(\)/) }) test('done row appears only after the full turn completes', () => { diff --git a/frontend/src/views/chat/components/RagPipelineProgress.vue b/frontend/src/views/chat/components/RagPipelineProgress.vue index babb464a52c..0773e252a5e 100644 --- a/frontend/src/views/chat/components/RagPipelineProgress.vue +++ b/frontend/src/views/chat/components/RagPipelineProgress.vue @@ -20,7 +20,7 @@
@@ -54,19 +54,17 @@
-
+
- {{ t('chat.connectingModelAndGeneratingAnswer') }} + {{ waitStepText }}
@@ -219,10 +217,14 @@ import { getRetrievalSearchSource, } from '@/utils/agent-tool-display' import { getAttachmentParsingSummaryHtml } from '@/utils/attachmentParsingDisplay' -import { RAG_TIMELINE_TOOL_NAMES } from '@/utils/rag-pipeline-history' +import { RAG_RETRIEVAL_TOOL_NAMES, RAG_TIMELINE_TOOL_NAMES } from '@/utils/rag-pipeline-history' import { useChatReferencesDrawer } from '@/composables/useChatReferencesDrawer' import { buildReferenceSections } from '@/utils/referenceSources' -import { shouldShowRagModelAnswerWait } from '@/utils/rag-pipeline-state' +import { + createRagWaitController, + getRagPipelineWaitKind, + type RagWaitView, +} from '@/utils/rag-pipeline-state' const props = defineProps<{ session?: { @@ -240,8 +242,10 @@ const referencesDrawer = useChatReferencesDrawer() const userExpanded = ref(false) const thinkingExpanded = ref(true) const rootElement = ref(null) -const showModelAnswerWait = ref(false) -let modelAnswerWaitTimer: ReturnType | undefined +const waitView = ref({ kind: 'none', stalled: false }) +const waitController = createRagWaitController((view) => { + waitView.value = view +}) const thinkingContent = computed(() => { const stream = props.session?.agentEventStream @@ -299,7 +303,7 @@ const steps = computed(() => { ? (event.tool_data as Record) : null - const isSearchTool = toolName === 'knowledge_search' || toolName === 'search_knowledge' + const isSearchTool = RAG_RETRIEVAL_TOOL_NAMES.has(toolName) const isAttachmentTool = toolName === 'attachment_parsing' || toolName === 'image_analysis' const searchSource = isSearchTool ? getRetrievalSearchSource(event.arguments, toolData) @@ -335,12 +339,10 @@ const allStepsDone = computed( ) const hasCompletedRetrievalStep = computed(() => steps.value.some( - (step) => - (step.toolName === 'knowledge_search' || step.toolName === 'search_knowledge') && - !step.pending, + (step) => RAG_RETRIEVAL_TOOL_NAMES.has(step.toolName) && !step.pending, )) -const shouldShowModelAnswerWait = computed(() => shouldShowRagModelAnswerWait({ +const waitKind = computed(() => getRagPipelineWaitKind({ isCompleted: Boolean(props.session?.is_completed), hasAnswer: hasAnswer.value, hasThinkingEvent: hasThinkingEvent.value, @@ -349,6 +351,17 @@ const shouldShowModelAnswerWait = computed(() => shouldShowRagModelAnswerWait({ hasCompletedRetrievalStep: hasCompletedRetrievalStep.value, })) +const showWaitStep = computed(() => waitView.value.kind !== 'none') + +const waitStepStalled = computed(() => waitView.value.stalled) + +const waitStepText = computed(() => { + if (waitView.value.stalled) return t('chat.modelStillResponding') + return waitView.value.kind === 'model' + ? t('chat.connectingModelAndGeneratingAnswer') + : t('chat.preparingAnswer') +}) + const showCollapsedRoot = computed( () => (hasAnswer.value || Boolean(props.session?.is_completed)) && @@ -463,26 +476,7 @@ watch(thinkingPending, (pending) => { } }) -watch(shouldShowModelAnswerWait, (shouldShow) => { - if (modelAnswerWaitTimer) { - clearTimeout(modelAnswerWaitTimer) - modelAnswerWaitTimer = undefined - } - - if (!shouldShow) { - showModelAnswerWait.value = false - return - } - - // Avoid flashing the model-wait row when the first answer token arrives - // immediately after retrieval completes. - modelAnswerWaitTimer = setTimeout(() => { - modelAnswerWaitTimer = undefined - if (shouldShowModelAnswerWait.value) { - showModelAnswerWait.value = true - } - }, 250) -}, { immediate: true }) +watch(waitKind, (kind) => waitController.update(kind), { immediate: true }) watch(hasAnswer, (answered) => { if (answered && hasThinking.value) { @@ -501,7 +495,7 @@ watch(thinkingExpanded, (expanded) => { }) onBeforeUnmount(() => { - if (modelAnswerWaitTimer) clearTimeout(modelAnswerWaitTimer) + waitController.dispose() }) From 35a041f15acfdc0ae1f14d772cb1f0a226e9d021 Mon Sep 17 00:00:00 2001 From: lyingbug <[email> Date: Tue, 4 Aug 2026 04:35:23 +0000 Subject: [PATCH 2/2] fix(frontend): announce RAG wait status from a persistent live region role="status" lived on the wait row itself, so the live region was inserted together with its own text and screen readers had nothing to announce. Move the announcement to an sr-only region that stays mounted for the whole turn, which also covers the pre-pipeline wait row that had no announcement at all. Co-authored-by: lyingbug --- .../RagPipelineProgress.style.test.mjs | 8 +++++++ .../chat/components/RagPipelineProgress.vue | 22 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/frontend/src/views/chat/components/RagPipelineProgress.style.test.mjs b/frontend/src/views/chat/components/RagPipelineProgress.style.test.mjs index de591cb7756..b29b837eea4 100644 --- a/frontend/src/views/chat/components/RagPipelineProgress.style.test.mjs +++ b/frontend/src/views/chat/components/RagPipelineProgress.style.test.mjs @@ -69,6 +69,14 @@ test('rag pipeline shows a pending model-answer step after retrieval completes', assert.match(source, /waitController\.dispose\(\)/) }) +test('rag pipeline announces wait status from a region that outlives each row', () => { + const template = source.split(' { assert.match(source, /const showDoneRow = computed\(\(\) => \{[\s\S]*hasAnswer\.value/) }) diff --git a/frontend/src/views/chat/components/RagPipelineProgress.vue b/frontend/src/views/chat/components/RagPipelineProgress.vue index 0773e252a5e..7e75ef52428 100644 --- a/frontend/src/views/chat/components/RagPipelineProgress.vue +++ b/frontend/src/views/chat/components/RagPipelineProgress.vue @@ -1,5 +1,9 @@