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

@@ -143,6 +143,7 @@ export function InputBox({
hideAdminRecommendedQuestions, hideAdminRecommendedQuestions,
compact, compact,
hideContextTools, hideContextTools,
hideMemoryTool = true,
...props ...props
}: Omit<ComponentProps<typeof PromptInput>, "onSubmit"> & { }: Omit<ComponentProps<typeof PromptInput>, "onSubmit"> & {
assistantId?: string | null; assistantId?: string | null;
@@ -153,6 +154,11 @@ export function InputBox({
/** 隐藏输入框工具条上的「写作 / 记忆 / 参考文献」上下文开关按钮(任务深链工作区等 /** 隐藏输入框工具条上的「写作 / 记忆 / 参考文献」上下文开关按钮(任务深链工作区等
* 受控场景用;文件/模型等其他按钮不受影响)。 */ * 受控场景用;文件/模型等其他按钮不受影响)。 */
hideContextTools?: boolean; hideContextTools?: boolean;
/**
* 仅控制「记忆」开关的可见性。产品当前全局隐藏,传入 false 即可无损恢复。
* 写作和参考文献开关不受影响。
*/
hideMemoryTool?: boolean;
disabled?: boolean; disabled?: boolean;
context: Omit< context: Omit<
AgentThreadContext, AgentThreadContext,
@@ -752,7 +758,7 @@ export function InputBox({
)} )}
/> />
)} )}
{!frontendRagActive && !hideContextTools && <Tooltip {!frontendRagActive && !hideContextTools && !hideMemoryTool && <Tooltip
content={ content={
context.writing_mode context.writing_mode
? "写作模式已开启:报告由深度研究管线按「通用分析结构」撰写,优先使用对话中已检索的资料" ? "写作模式已开启:报告由深度研究管线按「通用分析结构」撰写,优先使用对话中已检索的资料"
@@ -3,16 +3,18 @@ import { useEffect } from "react";
import { useI18n } from "@/core/i18n/hooks"; import { useI18n } from "@/core/i18n/hooks";
import type { AgentThreadState } from "@/core/threads"; import type { AgentThreadState } from "@/core/threads";
import { sanitizeThreadTitleForDisplay } from "@/core/threads/utils"; import { sanitizeThreadTitleForDisplay } from "@/core/threads/utils";
import { desensitize } from "@/lib/desensitize"; import { desensitize } from "@/lib/desensitize";
import { cn } from "@/lib/utils";
import { useThreadChat } from "./chats"; import { useThreadChat } from "./chats";
import { FlipDisplay } from "./flip-display"; import { FlipDisplay } from "./flip-display";
export function ThreadTitle({ export function ThreadTitle({
threadId, className,
thread, threadId,
}: { thread,
}: {
className?: string; className?: string;
threadId: string; threadId: string;
thread: BaseStream<AgentThreadState>; thread: BaseStream<AgentThreadState>;
@@ -47,9 +49,9 @@ export function ThreadTitle({
if (!thread.values?.title) { if (!thread.values?.title) {
return null; return null;
} }
const visibleTitle = desensitize(displayTitle); const visibleTitle = desensitize(displayTitle);
return ( return (
<FlipDisplay uniqueKey={threadId} className="min-w-0 w-full"> <FlipDisplay uniqueKey={threadId} className={cn("min-w-0 w-full", className)}>
<span className="block truncate" title={visibleTitle}> <span className="block truncate" title={visibleTitle}>
{visibleTitle} {visibleTitle}
</span> </span>
@@ -88,12 +88,12 @@ export const sidebarMenuItems: MenuItem[] = [
}, },
{ id: "qa-zzxd", label: "XD应用", icon: Layout, path: "/page/strategy/qa/zzxd" }, { id: "qa-zzxd", label: "XD应用", icon: Layout, path: "/page/strategy/qa/zzxd" },
{ id: "qa-research", label: "课题研究", icon: FileText, path: "/page/canvas/ai-writing" }, { id: "qa-research", label: "课题研究", icon: FileText, path: "/page/canvas/ai-writing" },
{ // {
id: "qa-research-chat", // id: "qa-research-chat",
label: "课题研究(对话版)", // label: "课题研究(对话版)",
icon: FileText, // icon: FileText,
path: "/page/canvas/ai-writing-chat", // path: "/page/canvas/ai-writing-chat",
}, // },
{ {
id: "qa-deep-research", id: "qa-deep-research",
label: "深度研究", label: "深度研究",
+48 -14
View File
@@ -38,12 +38,16 @@ import { CompactModeGuide } from "@/components/workspace/compact-mode-guide";
import { Welcome } from "@/components/workspace/welcome"; import { Welcome } from "@/components/workspace/welcome";
import { CompactWelcome } from "@/components/workspace/compact-welcome"; import { CompactWelcome } from "@/components/workspace/compact-welcome";
import { readSidebarEnabled } from "@/core/page-layout/sidebar-enabled"; 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 { isEmbedActive, isHideChromeActive } from "@/core/embed/embed-session";
import { isWujieEnv } from "@/core/embed/wujie"; import { isWujieEnv } from "@/core/embed/wujie";
import { useSidebar } from "@/components/ui/sidebar"; import { useSidebar } from "@/components/ui/sidebar";
import { Button } from "@/components/ui/button"; 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 { useAgentSelector, useAgentsPaginated } from "@/core/agents";
import { useI18n } from "@/core/i18n/hooks"; import { useI18n } from "@/core/i18n/hooks";
import { useModels } from "@/core/models/hooks"; import { useModels } from "@/core/models/hooks";
@@ -114,6 +118,7 @@ function clearBrowserContextParamsFromHash() {
export default function ChatPage() { export default function ChatPage() {
const routeBase = useRouteBase(); const routeBase = useRouteBase();
const router = useRouter(); const router = useRouter();
const navigate = useNavigate();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const pathname = usePathname(); const pathname = usePathname();
// `chatMode=full` is used by an embedded standard chat. It intentionally // `chatMode=full` is used by an embedded standard chat. It intentionally
@@ -671,6 +676,20 @@ export default function ChatPage() {
chromeByPreference && !isChatsIframeWorkspacePath(pathname); chromeByPreference && !isChatsIframeWorkspacePath(pathname);
const compactLanding = isLandingState && compactChrome; const compactLanding = isLandingState && compactChrome;
const compactActionsHidden = isHideChromeActive(); 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 嵌入 // 简洁模式新手引导:仅面向「无全局导航栏/侧边栏」的正常简洁布局。iframe 嵌入
// (embed=1)、去 chrome(hideChrome=1)、强制收起侧栏(无界部署)等环境同样没有 // (embed=1)、去 chrome(hideChrome=1)、强制收起侧栏(无界部署)等环境同样没有
// 全局 chrome,但不该在外部系统里弹引导。 // 全局 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"> <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 && ( {compactChrome && !historySidebarOpen && !compactActionsHidden && (
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
size="icon-sm" size="icon-sm"
onClick={toggleHistorySidebar} onClick={toggleHistorySidebar}
title="打开历史记录" title="展开左侧栏"
aria-label="打开历史记录" aria-label="展开左侧栏"
className="text-muted-foreground hover:text-foreground" className="mr-1 shrink-0 text-muted-foreground hover:text-foreground"
> >
<PanelLeftOpen className="size-4" /> <PanelLeftOpen className="size-4" />
</Button> </Button>
)} )}
<ThreadTitle
className="-translate-y-1"
threadId={threadId}
thread={displayThread}
/>
</div>
<div className="flex shrink-0 items-center gap-2">
<TokenUsageIndicator <TokenUsageIndicator
enabled={tokenUsageEnabled} enabled={tokenUsageEnabled}
messages={displayThread.messages} 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} /> <ExportTrigger threadId={threadId} />
<KnowledgeSaveTrigger threadId={threadId} /> <KnowledgeSaveTrigger threadId={threadId} />
<ArtifactTrigger /> <ArtifactTrigger />
@@ -797,11 +832,10 @@ export default function ChatPage() {
isLandingState && isLandingState &&
!compactLanding && !compactLanding &&
"-translate-y-[calc(50vh-96px)]", "-translate-y-[calc(50vh-96px)]",
compactLanding // 新对话落地态在全量与简洁模式使用同一输入框宽度。
? "max-w-[calc(var(--container-width-xl)+150px)]" isLandingState
: isLandingState ? "max-w-[calc(var(--container-width-md)+150px)]"
? "max-w-[calc(var(--container-width-md)+150px)]" : "max-w-(--container-width-lg)",
: "max-w-(--container-width-lg)",
)} )}
> >
<div className="absolute -top-4 right-0 left-0 z-0"> <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. // @ts-ignore Node's test runner resolves the local .ts module directly.
import { buildIframeModeButtons } from "./iframe-chat-mode-buttons.ts"; 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({ const buttons = buildIframeModeButtons({
agentSelected: false, agentSelected: false,
writingModeEnabled: false, writingModeEnabled: false,
@@ -20,7 +20,6 @@ test("includes the context compression toggle in the iframe chat toolbar", () =>
buttons.map((button) => button.key), buttons.map((button) => button.key),
[ [
"writing", "writing",
"memory",
"references", "references",
"contextCompression", "contextCompression",
"pro", "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", () => { test("hides the references toggle when reference mode is disabled globally", () => {
const buttons = buildIframeModeButtons({ const buttons = buildIframeModeButtons({
agentSelected: false, agentSelected: false,
@@ -15,6 +15,8 @@ type BuildIframeModeButtonsInput = {
agentSelected: boolean; agentSelected: boolean;
writingModeEnabled: boolean; writingModeEnabled: boolean;
memoryInjectionEnabled: boolean; memoryInjectionEnabled: boolean;
/** 产品当前全局隐藏记忆入口;保留 false 供后续无损恢复。 */
hideMemoryButton?: boolean;
referenceModeEntryEnabled: boolean; referenceModeEntryEnabled: boolean;
ragModeEnabled: boolean; ragModeEnabled: boolean;
ragModeLocked: boolean; ragModeLocked: boolean;
@@ -26,6 +28,7 @@ export function buildIframeModeButtons({
agentSelected, agentSelected,
writingModeEnabled, writingModeEnabled,
memoryInjectionEnabled, memoryInjectionEnabled,
hideMemoryButton = true,
referenceModeEntryEnabled, referenceModeEntryEnabled,
ragModeEnabled, ragModeEnabled,
ragModeLocked, ragModeLocked,
@@ -65,7 +68,9 @@ export function buildIframeModeButtons({
active: agentSelected, active: agentSelected,
}, },
]; ];
return referenceModeEntryEnabled return buttons.filter(
? buttons (button) =>
: buttons.filter((button) => button.key !== "references"); (referenceModeEntryEnabled || button.key !== "references") &&
(!hideMemoryButton || button.key !== "memory"),
);
} }