feat(chat): 优化聊天页布局与 iframe 工具栏行为

This commit is contained in:
董宏凯 committed 2026-09-01 12:49:03 +08:00
1 parent 38857c425b
commit 051e66f7d8
6 files changed
+97 -35

No files matched your search

+48 -14
View File
@@ -38,12 +38,16 @@ import { CompactModeGuide } from "@/components/workspace/compact-mode-guide";
import { Welcome } from "@/components/workspace/welcome";
import { CompactWelcome } from "@/components/workspace/compact-welcome";
import { readSidebarEnabled } from "@/core/page-layout/sidebar-enabled";
import { isChatsIframeWorkspacePath } from "@/core/page-layout/chats-iframe-route";
import {
buildFullChatNavigationPath,
isChatsIframeWorkspacePath,
} from "@/core/page-layout/chats-iframe-route";
import { isEmbedActive, isHideChromeActive } from "@/core/embed/embed-session";
import { isWujieEnv } from "@/core/embed/wujie";
import { useSidebar } from "@/components/ui/sidebar";
import { Button } from "@/components/ui/button";
import { PanelLeftOpen } from "lucide-react";
import { MessageSquarePlus, PanelLeftOpen } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { useAgentSelector, useAgentsPaginated } from "@/core/agents";
import { useI18n } from "@/core/i18n/hooks";
import { useModels } from "@/core/models/hooks";
@@ -114,6 +118,7 @@ function clearBrowserContextParamsFromHash() {
export default function ChatPage() {
const routeBase = useRouteBase();
const router = useRouter();
const navigate = useNavigate();
const searchParams = useSearchParams();
const pathname = usePathname();
// `chatMode=full` is used by an embedded standard chat. It intentionally
@@ -671,6 +676,20 @@ export default function ChatPage() {
chromeByPreference && !isChatsIframeWorkspacePath(pathname);
const compactLanding = isLandingState && compactChrome;
const compactActionsHidden = isHideChromeActive();
const headerNewChatPath = useMemo(
() =>
buildFullChatNavigationPath(
`${routeBase}/chats/new`,
searchParams.toString(),
),
[routeBase, searchParams],
);
const handleHeaderNewChat = useCallback(() => {
navigate(headerNewChatPath, {
replace: true,
state: { resetAt: Date.now() },
});
}, [headerNewChatPath, navigate]);
// 简洁模式新手引导:仅面向「无全局导航栏/侧边栏」的正常简洁布局。iframe 嵌入
// (embed=1)、去 chrome(hideChrome=1)、强制收起侧栏(无界部署)等环境同样没有
// 全局 chrome,但不该在外部系统里弹引导。
@@ -709,27 +728,43 @@ export default function ChatPage() {
)}
>
<div className="flex min-w-0 flex-1 items-center overflow-hidden text-sm font-medium">
<ThreadTitle threadId={threadId} thread={displayThread} />
</div>
<div className="flex shrink-0 items-center gap-2">
{/* 仅在收起态显示「展开历史」钮;展开后由侧栏内(新对话右侧)的收起钮负责收起。 */}
{/* 左栏收起后,展开入口固定在聊天区左上角。 */}
{compactChrome && !historySidebarOpen && !compactActionsHidden && (
<Button
type="button"
variant="ghost"
size="icon-sm"
onClick={toggleHistorySidebar}
title="打开历史记录"
aria-label="打开历史记录"
className="text-muted-foreground hover:text-foreground"
title="展开左侧栏"
aria-label="展开左侧栏"
className="mr-1 shrink-0 text-muted-foreground hover:text-foreground"
>
<PanelLeftOpen className="size-4" />
</Button>
)}
<ThreadTitle
className="-translate-y-1"
threadId={threadId}
thread={displayThread}
/>
</div>
<div className="flex shrink-0 items-center gap-2">
<TokenUsageIndicator
enabled={tokenUsageEnabled}
messages={displayThread.messages}
/>
{compactChrome && !historySidebarOpen && !compactActionsHidden && (
<Button
type="button"
variant="ghost"
size="sm"
onClick={handleHeaderNewChat}
className="text-muted-foreground hover:text-foreground"
>
<MessageSquarePlus className="size-4" />
新建对话
</Button>
)}
<ExportTrigger threadId={threadId} />
<KnowledgeSaveTrigger threadId={threadId} />
<ArtifactTrigger />
@@ -797,11 +832,10 @@ export default function ChatPage() {
isLandingState &&
!compactLanding &&
"-translate-y-[calc(50vh-96px)]",
compactLanding
? "max-w-[calc(var(--container-width-xl)+150px)]"
: isLandingState
? "max-w-[calc(var(--container-width-md)+150px)]"
: "max-w-(--container-width-lg)",
// 新对话落地态在全量与简洁模式使用同一输入框宽度。
isLandingState
? "max-w-[calc(var(--container-width-md)+150px)]"
: "max-w-(--container-width-lg)",
)}
>
<div className="absolute -top-4 right-0 left-0 z-0">
@@ -4,7 +4,7 @@ import test from "node:test";
// @ts-ignore Node's test runner resolves the local .ts module directly.
import { buildIframeModeButtons } from "./iframe-chat-mode-buttons.ts";
test("includes the context compression toggle in the iframe chat toolbar", () => {
test("hides the memory toggle while retaining the other iframe chat toolbar controls", () => {
const buttons = buildIframeModeButtons({
agentSelected: false,
writingModeEnabled: false,
@@ -20,7 +20,6 @@ test("includes the context compression toggle in the iframe chat toolbar", () =>
buttons.map((button) => button.key),
[
"writing",
"memory",
"references",
"contextCompression",
"pro",
@@ -29,6 +28,22 @@ test("includes the context compression toggle in the iframe chat toolbar", () =>
);
});
test("can restore the iframe memory toggle when the feature is re-enabled", () => {
const buttons = buildIframeModeButtons({
agentSelected: false,
writingModeEnabled: false,
memoryInjectionEnabled: true,
hideMemoryButton: false,
referenceModeEntryEnabled: true,
ragModeEnabled: false,
ragModeLocked: false,
contextCompressionEnabled: false,
proModeEnabled: false,
});
assert.equal(buttons.some((button) => button.key === "memory"), true);
});
test("hides the references toggle when reference mode is disabled globally", () => {
const buttons = buildIframeModeButtons({
agentSelected: false,
@@ -15,6 +15,8 @@ type BuildIframeModeButtonsInput = {
agentSelected: boolean;
writingModeEnabled: boolean;
memoryInjectionEnabled: boolean;
/** 产品当前全局隐藏记忆入口;保留 false 供后续无损恢复。 */
hideMemoryButton?: boolean;
referenceModeEntryEnabled: boolean;
ragModeEnabled: boolean;
ragModeLocked: boolean;
@@ -26,6 +28,7 @@ export function buildIframeModeButtons({
agentSelected,
writingModeEnabled,
memoryInjectionEnabled,
hideMemoryButton = true,
referenceModeEntryEnabled,
ragModeEnabled,
ragModeLocked,
@@ -65,7 +68,9 @@ export function buildIframeModeButtons({
active: agentSelected,
},
];
return referenceModeEntryEnabled
? buttons
: buttons.filter((button) => button.key !== "references");
return buttons.filter(
(button) =>
(referenceModeEntryEnabled || button.key !== "references") &&
(!hideMemoryButton || button.key !== "memory"),
);
}