feat: 聊天增加回到最新悬浮钮,智能体面板生成即收起

- 复用 ConversationScrollButton 接入 MessageList,滚离底部时显示一键回到最新问答的悬浮按钮;新增 scrollButtonClassName 以适配各页输入框高度
- 智能体详情面板改用 open+selectedArtifact 判定收起,流式写文件开始即收起让位预览,不再等生成结束

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
吉鑫升andClaude Opus 4.8 committed 2026-06-16 19:24:48 +08:00
1 parent d82b5ed884
commit 4f0494a97a
4 files changed
+33 -8

No files matched your search

@@ -187,23 +187,30 @@ export function AgentDetailPanel({
const isSaving = updateAgent.isPending || setFeatured.isPending;
// 对话生成文件(md/txt 等 artifact)并在右侧展开预览时,自动收起智能体
// 信息面板给预览让位;新文件落地时同样收起。编辑中不打断用户,用户随时
// 信息面板给预览让位;切换到新文件时同样收起。编辑中不打断用户,用户随时
// 可手动重新展开。
//
// 关键:判定用 open + selectedArtifact,而非 artifacts.length。流式写文件时
// message-group 一开始就调用 select()+setOpen(true) 打开预览并选中该文件,但
// 该文件要等生成结束、artifact 入库后才会进 thread.values.artifacts。若按
// 数量判定就只能等生成结束才收起——正是之前「生成完才收起」的 bug 根因。
const artifactsCtx = useArtifactsOptional();
const artifactCount = artifactsCtx?.artifacts.length ?? 0;
const artifactPanelVisible = !!artifactsCtx?.open && artifactCount > 0;
const selectedArtifact = artifactsCtx?.selectedArtifact ?? null;
const artifactPanelVisible =
!!artifactsCtx?.open && (!!selectedArtifact || artifactCount > 0);
const prevArtifactPanelVisible = useRef(false);
const prevArtifactCount = useRef(0);
const prevSelectedArtifact = useRef<string | null>(null);
useEffect(() => {
const opened = artifactPanelVisible && !prevArtifactPanelVisible.current;
const grew =
artifactPanelVisible && artifactCount > prevArtifactCount.current;
const switched =
artifactPanelVisible && selectedArtifact !== prevSelectedArtifact.current;
prevArtifactPanelVisible.current = artifactPanelVisible;
prevArtifactCount.current = artifactCount;
if ((opened || grew) && !editing) {
prevSelectedArtifact.current = selectedArtifact;
if ((opened || switched) && !editing) {
setCollapsed(true);
}
}, [artifactPanelVisible, artifactCount, editing]);
}, [artifactPanelVisible, selectedArtifact, editing]);
const displayName = desensitize(agent?.name ?? agentName);
const questions = useMemo(
@@ -6,6 +6,7 @@ import { useCallback, useContext, useEffect, useMemo, useState } from "react";
import {
Conversation,
ConversationContent,
ConversationScrollButton,
} from "@/components/ai-elements/conversation";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
@@ -61,6 +62,7 @@ export function MessageList({
paddingBottom = MESSAGE_LIST_DEFAULT_PADDING_BOTTOM,
tokenUsageEnabled = false,
extraRehypePlugins,
scrollButtonClassName,
onClarificationSubmit,
onEditResubmit,
onSetupAgentApprove,
@@ -75,6 +77,8 @@ export function MessageList({
thread: BaseStream<AgentThreadState>;
paddingBottom?: number;
tokenUsageEnabled?: boolean;
/** 额外定位「回到最新」悬浮按钮——不同页面输入框高度不同,需上移让位。 */
scrollButtonClassName?: string;
extraRehypePlugins?: ReturnType<typeof useRehypeSplitWordsIntoSpans>;
onClarificationSubmit?: (answer: string) => void | Promise<void>;
onEditResubmit?: (text: string) => void;
@@ -543,6 +547,16 @@ export function MessageList({
<div style={{ height: `${paddingBottom}px` }} />
</MessageMetricsProvider>
</ConversationContent>
{/* ChatGPT 式「回到最新」悬浮钮:仅在向上滚离底部时出现,点击平滑回到
最新问答。默认贴底,调用方可用 scrollButtonClassName 上移避开输入框。 */}
<ConversationScrollButton
className={cn(
"pointer-events-auto z-20 size-9 shadow-md",
scrollButtonClassName,
)}
aria-label="回到最新"
title="回到最新"
/>
</Conversation>
);
}
+2
View File
@@ -285,6 +285,8 @@ export default function AgentChatPage() {
thread={thread}
paddingBottom={showFollowups ? 96 : 24}
tokenUsageEnabled={tokenUsageEnabled}
// 输入框在流内(MessageList 下方),按钮贴对话区底部即可。
scrollButtonClassName="bottom-2"
onClarificationSubmit={handleClarificationSubmit}
onEditResubmit={(text) => handleSubmit({ text, files: [] })}
/>
+2
View File
@@ -493,6 +493,8 @@ export default function ChatPage() {
thread={thread}
paddingBottom={messageListPaddingBottom}
tokenUsageEnabled={tokenUsageEnabled}
// 输入框为底部绝对浮层(约 160px 高),按钮上移到其上方居中。
scrollButtonClassName="bottom-40"
onClarificationSubmit={handleClarificationSubmit}
onEditResubmit={(text) =>
handleSubmit({ text, files: [] })