feat(chat): 优化聊天页布局与 iframe 工具栏行为
This commit is contained in:
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: "深度研究",
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user