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,
compact,
hideContextTools,
hideMemoryTool = true,
...props
}: Omit<ComponentProps<typeof PromptInput>, "onSubmit"> & {
assistantId?: string | null;
@@ -153,6 +154,11 @@ export function InputBox({
/** 隐藏输入框工具条上的「写作 / 记忆 / 参考文献」上下文开关按钮(任务深链工作区等
* 受控场景用;文件/模型等其他按钮不受影响)。 */
hideContextTools?: boolean;
/**
* 仅控制「记忆」开关的可见性。产品当前全局隐藏,传入 false 即可无损恢复。
* 写作和参考文献开关不受影响。
*/
hideMemoryTool?: boolean;
disabled?: boolean;
context: Omit<
AgentThreadContext,
@@ -752,7 +758,7 @@ export function InputBox({
)}
/>
)}
{!frontendRagActive && !hideContextTools && <Tooltip
{!frontendRagActive && !hideContextTools && !hideMemoryTool && <Tooltip
content={
context.writing_mode
? "写作模式已开启:报告由深度研究管线按「通用分析结构」撰写,优先使用对话中已检索的资料"
@@ -3,16 +3,18 @@ import { useEffect } from "react";
import { useI18n } from "@/core/i18n/hooks";
import type { AgentThreadState } from "@/core/threads";
import { sanitizeThreadTitleForDisplay } from "@/core/threads/utils";
import { desensitize } from "@/lib/desensitize";
import { sanitizeThreadTitleForDisplay } from "@/core/threads/utils";
import { desensitize } from "@/lib/desensitize";
import { cn } from "@/lib/utils";
import { useThreadChat } from "./chats";
import { FlipDisplay } from "./flip-display";
export function ThreadTitle({
threadId,
thread,
}: {
export function ThreadTitle({
className,
threadId,
thread,
}: {
className?: string;
threadId: string;
thread: BaseStream<AgentThreadState>;
@@ -47,9 +49,9 @@ export function ThreadTitle({
if (!thread.values?.title) {
return null;
}
const visibleTitle = desensitize(displayTitle);
return (
<FlipDisplay uniqueKey={threadId} className="min-w-0 w-full">
const visibleTitle = desensitize(displayTitle);
return (
<FlipDisplay uniqueKey={threadId} className={cn("min-w-0 w-full", className)}>
<span className="block truncate" title={visibleTitle}>
{visibleTitle}
</span>
@@ -88,12 +88,12 @@ export const sidebarMenuItems: MenuItem[] = [
},
{ 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-chat",
label: "课题研究(对话版)",
icon: FileText,
path: "/page/canvas/ai-writing-chat",
},
// {
// id: "qa-research-chat",
// label: "课题研究(对话版)",
// icon: FileText,
// path: "/page/canvas/ai-writing-chat",
// },
{
id: "qa-deep-research",
label: "深度研究",
+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"),
);
}