From b054b98cae95a0b6e8c7c9b5736be57a8d632d2d Mon Sep 17 00:00:00 2001 From: HyxiaoGe <18889592303@163.com> Date: Sun, 11 Oct 2026 22:21:42 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=9B=9E=E7=AD=94=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E5=90=8E=E5=88=B7=E6=96=B0=E4=BC=9A=E8=AF=9D=E6=97=B6=E6=8E=A8?= =?UTF-8?q?=E8=8D=90=E9=97=AE=E9=A2=98=E4=B8=8D=E5=86=8D=E9=97=AA=E4=B8=80?= =?UTF-8?q?=E4=B8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../src/app/(app)/chat/[chatId]/page.test.tsx | 20 +++++++++++++++++++ frontend/src/app/(app)/chat/[chatId]/page.tsx | 10 ++++++---- .../chat/SuggestedQuestions.test.tsx | 11 ++++++++++ .../components/chat/SuggestedQuestions.tsx | 5 ++++- 4 files changed, 41 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/(app)/chat/[chatId]/page.test.tsx b/frontend/src/app/(app)/chat/[chatId]/page.test.tsx index a5a65d920..4103138a3 100644 --- a/frontend/src/app/(app)/chat/[chatId]/page.test.tsx +++ b/frontend/src/app/(app)/chat/[chatId]/page.test.tsx @@ -2418,6 +2418,26 @@ describe('ChatPage 会话切换体验', () => { expect(lastMessageListProps?.loadingState).toBeUndefined(); }); + it('同一会话回答完成后的权威刷新期间保留推荐问题', async () => { + conversationsById.set('chat-a', createConversation('chat-a', [textMessage('message-a')])); + hydrationById.set('chat-a', { view: 'ready' }); + suggestedQuestionsState.questions = ['继续问']; + + const { rerender } = render(); + + await waitFor(() => { + expect(screen.getByTestId('message-list')).toHaveAttribute('data-message-ids', 'message-a'); + }); + + hydrationById.set('chat-a', { view: 'loading' }); + rerender(); + + const lastMessageListProps = chatMessageListMock.mock.calls.at(-1)?.[0]; + expect(lastMessageListProps?.loadingState).toBeUndefined(); + expect(lastMessageListProps?.suggestedQuestions).toEqual(['继续问']); + expect(lastMessageListProps?.onSelectQuestion).toEqual(expect.any(Function)); + }); + it('动态 page remount 后切到未加载过的 loading 会话时不显示上一段 ready messages', async () => { conversationsById.set('chat-a', createConversation('chat-a', [textMessage('message-a')])); hydrationById.set('chat-a', { view: 'ready' }); diff --git a/frontend/src/app/(app)/chat/[chatId]/page.tsx b/frontend/src/app/(app)/chat/[chatId]/page.tsx index 7d9e2936d..33d7420a2 100644 --- a/frontend/src/app/(app)/chat/[chatId]/page.tsx +++ b/frontend/src/app/(app)/chat/[chatId]/page.tsx @@ -1356,10 +1356,12 @@ export default function ChatPage() { ? undefined : handleInspectTrajectory } - suggestedQuestions={shouldKeepPreviousContent || isHydratingWithoutContent ? [] : suggestedQuestions} - isLoadingQuestions={shouldKeepPreviousContent || isHydratingWithoutContent ? false : isLoadingQuestions} - onSelectQuestion={shouldKeepPreviousContent || isHydratingWithoutContent ? undefined : handleSelectQuestion} - onRefreshQuestions={shouldKeepPreviousContent || isHydratingWithoutContent ? undefined : handleRefreshQuestions} + // 保留上一屏的一定是同一会话(回答完成后的权威刷新),推荐问题属于它, + // 照常显示,否则刷新的一瞬间推荐区会消失再出现。 + suggestedQuestions={isHydratingWithoutContent ? [] : suggestedQuestions} + isLoadingQuestions={isHydratingWithoutContent ? false : isLoadingQuestions} + onSelectQuestion={isHydratingWithoutContent ? undefined : handleSelectQuestion} + onRefreshQuestions={isHydratingWithoutContent ? undefined : handleRefreshQuestions} completionStateVisible={shouldKeepPreviousContent || isHydratingWithoutContent ? false : showCompletionState} notificationReadEnabled={notificationReadReady && hydrationStatus === 'done' && !shouldKeepPreviousContent && activeSurface === 'chat'} notificationSessionKey={authSessionKey} diff --git a/frontend/src/components/chat/SuggestedQuestions.test.tsx b/frontend/src/components/chat/SuggestedQuestions.test.tsx index 27ff6efb8..848aca03d 100644 --- a/frontend/src/components/chat/SuggestedQuestions.test.tsx +++ b/frontend/src/components/chat/SuggestedQuestions.test.tsx @@ -174,6 +174,17 @@ describe('SuggestedQuestions', () => { expect(placeholder.querySelectorAll('.h-8')).toHaveLength(3); }); + it('fades in questions only when they arrive after the placeholder', () => { + const { rerender } = render( + , + ); + rerender(); + expect(screen.getByRole('button', { name: /后到的问题/ }).className).toContain('fade-in'); + + render(); + expect(screen.getByRole('button', { name: /已有的问题/ }).className).not.toContain('fade-in'); + }); + it('uses stronger pending affordance after selecting a question', () => { const onSelectQuestion = vi.fn(); diff --git a/frontend/src/components/chat/SuggestedQuestions.tsx b/frontend/src/components/chat/SuggestedQuestions.tsx index 403dce2cb..a0e95e494 100644 --- a/frontend/src/components/chat/SuggestedQuestions.tsx +++ b/frontend/src/components/chat/SuggestedQuestions.tsx @@ -28,6 +28,8 @@ const SuggestedQuestions: React.FC = ({ const [hoveredIndex, setHoveredIndex] = useState(null); const [isRefreshing, setIsRefreshing] = useState(false); const [pendingQuestion, setPendingQuestion] = useState(null); + // 只有先占位、后到达的问题才淡入;挂载时已有问题(流结束切换渲染分支、刷新页面)直接显示,不重播动画。 + const [fadeInArrivals] = useState(() => questions.length === 0); const { isAuthenticated } = useAppSelector((state) => state.auth); const { toast } = useToast(); const isBusy = isLoading || isRefreshing || pendingQuestion !== null; @@ -137,7 +139,8 @@ const SuggestedQuestions: React.FC = ({ size="sm" className={cn( "flex items-center gap-2 w-full text-left rounded-md border border-border/50 bg-transparent px-2.5 py-1.5 text-sm text-foreground transition-colors duration-fast hover:bg-muted/30 hover:border-border", - "h-auto justify-start font-normal disabled:opacity-75 animate-in fade-in duration-300 motion-reduce:animate-none", + "h-auto justify-start font-normal disabled:opacity-75", + fadeInArrivals && "animate-in fade-in duration-300 motion-reduce:animate-none", pendingQuestion === question && "border-info-border bg-info-bg text-info" )} onClick={() => handleQuestionSelect(question)}