diff --git a/frontend/src/app/(app)/chat/[chatId]/page.test.tsx b/frontend/src/app/(app)/chat/[chatId]/page.test.tsx
index a5a65d92..4103138a 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 7d9e2936..33d7420a 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 27ff6efb..848aca03 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 403dce2c..a0e95e49 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)}