fix: 嵌套深链改为按路由去 chrome,顶层不再清除共用 embed.session
避免全量父页清掉老版内层深链仍依赖的 embed.session;深链问答/会商/大屏绘制由 isHostNestedSurfacePath 控制 chrome,并抽取对话历史 Tab 共享行组件。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
1764d0d5f2
commit
37517c2778
8 files changed
+695
-250
No files matched your search
@@ -16,6 +16,7 @@ import {
|
||||
readHideHeaderParam,
|
||||
readHideChromeParam,
|
||||
} from "@/core/embed/embed-session";
|
||||
import { isHostNestedSurfacePath } from "@/core/page-layout/host-nested-surface";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useActiveThirdLevel } from "@/core/page-layout/use-sidebar-menu";
|
||||
@@ -91,23 +92,20 @@ function PageLayout() {
|
||||
location.pathname.startsWith("/page/workspace/chats/")
|
||||
);
|
||||
}, [location.pathname, location.search]);
|
||||
// 「大屏绘制」独立页(/page/strategy/dashboard-agent)本就是「带导航栏 + 侧边栏」的独立页,
|
||||
// 但它常被深链「上一步」从去 chrome 的工作区/新标签打开——那里 chromeByPreference 可能为 false
|
||||
// (界面模式非全量、或会话级 sidebar 未置位),导致整页没有顶栏/侧栏。故对该路由**强制全局 chrome**
|
||||
// (仍尊重 embed=1 嵌入态:外部 iframe 套用时仍隐藏)。
|
||||
const forceChromeForDashboardAgent =
|
||||
!forceCollapsedSidebar && location.pathname.includes("/strategy/dashboard-agent");
|
||||
// 深链嵌套页(单智能体问答 / 多智能体会商 / 大屏绘制)只给宿主 iframe 套用,
|
||||
// 路径上就不带全局 Header / PageSidebar,不靠改写共用 embed.session。
|
||||
const hostNestedSurface = isHostNestedSurfacePath(location.pathname);
|
||||
const pageSidebarAllowed =
|
||||
(forceChromeForDashboardAgent || chromeByPreference) &&
|
||||
chromeByPreference &&
|
||||
!isChatsIframeWorkspacePath(location.pathname) &&
|
||||
!embedActive &&
|
||||
// `?hideChrome=1` 显式去 chrome:优先级高于上面对 dashboard-agent 的强制全局 chrome。
|
||||
!chromeHidden;
|
||||
!chromeHidden &&
|
||||
!hostNestedSurface;
|
||||
const compactToolboxAllowed =
|
||||
!chromeByPreference &&
|
||||
!forceChromeForDashboardAgent &&
|
||||
!embedActive &&
|
||||
!chromeHidden &&
|
||||
!hostNestedSurface &&
|
||||
!isChatsIframeWorkspacePath(location.pathname);
|
||||
|
||||
// h-full(100%)而非 h-screen(100vh):配合 globals.css 的 html/body/#root height:100%
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* 「对话历史」页 —— 多智能体会商 Tab。
|
||||
*
|
||||
* 数据与原侧栏「多智能体会商」列表同源(当前用户的个人草稿 roundtable_drafts,
|
||||
* useRoundtableDrafts 不带 taskId);点击行为也一致:暂存 sessionStorage
|
||||
* 「roundtable:pending-draft」后跳转会商页,由页面挂载时消费并加载该草稿。
|
||||
* 任务维度会商(task-draft,按 taskId 全任务共享、不分权)不随个人历史走,
|
||||
* 仍只在对应任务工作区里查看。
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Share2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { ShareRoundtableDraftDialog } from "@/components/workspace/roundtable-draft-share-dialogs";
|
||||
import { formatRelative } from "@/open-canvas/components/ai-writing/AIWritingHistoryDropdown";
|
||||
import { useRoundtableDrafts } from "@/roundtable-planning/hooks/useRoundtableDrafts";
|
||||
import type { DraftMeta } from "@/roundtable-planning/api/drafts";
|
||||
import { desensitize } from "@/lib/desensitize";
|
||||
|
||||
import {
|
||||
DeleteIcon,
|
||||
HistoryRenameDialog,
|
||||
HistoryRow,
|
||||
HistoryRowIconButton,
|
||||
HistoryStatusPill,
|
||||
RenameIcon,
|
||||
} from "./history-tab-shared";
|
||||
|
||||
const ROUNDTABLE_PATH = "/page/workspace/roundtable/planning";
|
||||
|
||||
/** 草稿后台作业状态 → 小标签(映射与原侧栏列表一致)。 */
|
||||
function statusOf(draft: DraftMeta): { label: string; className: string } {
|
||||
if (draft.status === "running" || draft.status === "queued") {
|
||||
return { label: "进行中", className: "text-primary" };
|
||||
}
|
||||
if (draft.status === "awaiting_input") {
|
||||
return { label: "待澄清", className: "text-amber-500" };
|
||||
}
|
||||
if (draft.status === "error" || draft.status === "cancelled") {
|
||||
return { label: "出错", className: "text-rose-500" };
|
||||
}
|
||||
// 无进行中作业时展示本次会商所用业务链(无链条 → 自由模式)。
|
||||
return { label: draft.chainTitle || "自由模式", className: "text-muted-foreground" };
|
||||
}
|
||||
|
||||
export function RoundtableHistoryTab({
|
||||
query,
|
||||
onTotalChange,
|
||||
}: {
|
||||
query: string;
|
||||
onTotalChange?: (total: number) => void;
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
const { drafts, loading, refresh, rename, remove } = useRoundtableDrafts();
|
||||
const [renameId, setRenameId] = useState<string | null>(null);
|
||||
const [renameTitle, setRenameTitle] = useState("");
|
||||
const [shareId, setShareId] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
void refresh();
|
||||
}, [refresh]);
|
||||
|
||||
const keyword = query.trim().toLowerCase();
|
||||
const filtered = keyword
|
||||
? drafts.filter((draft) =>
|
||||
(draft.title || "未命名草稿").toLowerCase().includes(keyword),
|
||||
)
|
||||
: drafts;
|
||||
|
||||
useEffect(() => {
|
||||
onTotalChange?.(filtered.length);
|
||||
}, [filtered.length, onTotalChange]);
|
||||
|
||||
const openDraft = (draft: DraftMeta) => {
|
||||
sessionStorage.setItem("roundtable:pending-draft", draft.id);
|
||||
navigate(ROUNDTABLE_PATH);
|
||||
};
|
||||
|
||||
const handleRename = async (id: string, title: string) => {
|
||||
try {
|
||||
await rename(id, title);
|
||||
toast.success("已重命名");
|
||||
} catch {
|
||||
toast.error("重命名失败,请重试");
|
||||
} finally {
|
||||
setRenameId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (draft: DraftMeta) => {
|
||||
if (!window.confirm("确定删除这条会商记录吗?此操作不可恢复。")) return;
|
||||
try {
|
||||
await remove(draft.id);
|
||||
toast.success("已删除会商记录");
|
||||
} catch {
|
||||
toast.error("删除失败,请重试");
|
||||
}
|
||||
};
|
||||
|
||||
if (loading && drafts.length === 0) {
|
||||
return (
|
||||
<div className="text-muted-foreground py-16 text-center text-sm">
|
||||
加载中…
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (filtered.length === 0) {
|
||||
return (
|
||||
<div className="text-muted-foreground py-16 text-center text-sm">
|
||||
{keyword ? "没有匹配的会商记录" : "暂无会商记录"}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{filtered.map((draft) => {
|
||||
const status = statusOf(draft);
|
||||
return (
|
||||
<HistoryRow
|
||||
key={draft.id}
|
||||
title={desensitize(draft.title || "未命名草稿")}
|
||||
meta={
|
||||
<>
|
||||
<HistoryStatusPill
|
||||
label={status.label}
|
||||
className={status.className}
|
||||
/>
|
||||
<span className="text-muted-foreground/60">·</span>
|
||||
<span>{formatRelative(draft.updatedAt)}</span>
|
||||
</>
|
||||
}
|
||||
onOpen={() => openDraft(draft)}
|
||||
>
|
||||
<HistoryRowIconButton
|
||||
title="重命名"
|
||||
onClick={() => {
|
||||
setRenameId(draft.id);
|
||||
setRenameTitle(draft.title || "");
|
||||
}}
|
||||
>
|
||||
<RenameIcon />
|
||||
</HistoryRowIconButton>
|
||||
<HistoryRowIconButton title="分享" onClick={() => setShareId(draft.id)}>
|
||||
<Share2 className="text-muted-foreground size-4" />
|
||||
</HistoryRowIconButton>
|
||||
<HistoryRowIconButton
|
||||
title="删除"
|
||||
onClick={() => void handleDelete(draft)}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</HistoryRowIconButton>
|
||||
</HistoryRow>
|
||||
);
|
||||
})}
|
||||
|
||||
<HistoryRenameDialog
|
||||
open={renameId !== null}
|
||||
initialTitle={renameTitle}
|
||||
dialogTitle="重命名会商"
|
||||
onCancel={() => setRenameId(null)}
|
||||
onConfirm={(title) => renameId && void handleRename(renameId, title)}
|
||||
/>
|
||||
|
||||
<ShareRoundtableDraftDialog
|
||||
draftId={shareId}
|
||||
open={shareId !== null}
|
||||
onOpenChange={(o) => {
|
||||
if (!o) setShareId(null);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* 「对话历史」页 —— AI写作 Tab。
|
||||
*
|
||||
* 数据与原侧栏「最近写作」列表同源(useAIWritingSessions);点击行为一致:
|
||||
* 暂存 sessionStorage「ai-writing:pending-session」后跳转 AI 写作页,由
|
||||
* AIWritingPanel 挂载时消费并打开该会话。
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import {
|
||||
formatRelative,
|
||||
STATUS_LABELS,
|
||||
} from "@/open-canvas/components/ai-writing/AIWritingHistoryDropdown";
|
||||
import { useAIWritingSessions } from "@/open-canvas/hooks/useAIWritingSessions";
|
||||
import type { AIWritingSessionSummary } from "@/open-canvas/api/ai-writing-sessions";
|
||||
import { desensitize } from "@/lib/desensitize";
|
||||
|
||||
import {
|
||||
DeleteIcon,
|
||||
HistoryRenameDialog,
|
||||
HistoryRow,
|
||||
HistoryRowIconButton,
|
||||
HistoryStatusPill,
|
||||
RenameIcon,
|
||||
} from "./history-tab-shared";
|
||||
|
||||
const AI_WRITING_PATH = "/page/workspace/ai-writing";
|
||||
|
||||
export function WritingHistoryTab({
|
||||
query,
|
||||
onTotalChange,
|
||||
}: {
|
||||
query: string;
|
||||
onTotalChange?: (total: number) => void;
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
const { sessions, loading, refresh, rename, remove } = useAIWritingSessions();
|
||||
const [renameId, setRenameId] = useState<string | null>(null);
|
||||
const [renameTitle, setRenameTitle] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
void refresh();
|
||||
}, [refresh]);
|
||||
|
||||
const keyword = query.trim().toLowerCase();
|
||||
const filtered = keyword
|
||||
? sessions.filter((session) => session.title.toLowerCase().includes(keyword))
|
||||
: sessions;
|
||||
|
||||
useEffect(() => {
|
||||
onTotalChange?.(filtered.length);
|
||||
}, [filtered.length, onTotalChange]);
|
||||
|
||||
const openSession = (session: AIWritingSessionSummary) => {
|
||||
sessionStorage.setItem("ai-writing:pending-session", JSON.stringify(session));
|
||||
navigate(AI_WRITING_PATH);
|
||||
};
|
||||
|
||||
const handleRename = async (id: string, title: string) => {
|
||||
try {
|
||||
await rename(id, title);
|
||||
toast.success("已重命名");
|
||||
} catch {
|
||||
toast.error("重命名失败,请重试");
|
||||
} finally {
|
||||
setRenameId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (session: AIWritingSessionSummary) => {
|
||||
if (!window.confirm("确定删除这条写作记录吗?此操作不可恢复。")) return;
|
||||
try {
|
||||
await remove(session.id);
|
||||
toast.success("已删除写作记录");
|
||||
} catch {
|
||||
toast.error("删除失败,请重试");
|
||||
}
|
||||
};
|
||||
|
||||
if (loading && sessions.length === 0) {
|
||||
return (
|
||||
<div className="text-muted-foreground py-16 text-center text-sm">
|
||||
加载中…
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (filtered.length === 0) {
|
||||
return (
|
||||
<div className="text-muted-foreground py-16 text-center text-sm">
|
||||
{keyword ? "没有匹配的写作记录" : "暂无写作记录"}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{filtered.map((session) => {
|
||||
const { label, color } = STATUS_LABELS[session.status] ?? {
|
||||
label: session.status,
|
||||
color: "text-muted-foreground",
|
||||
};
|
||||
return (
|
||||
<HistoryRow
|
||||
key={session.id}
|
||||
title={desensitize(session.title)}
|
||||
meta={
|
||||
<>
|
||||
<HistoryStatusPill label={label} className={color} />
|
||||
<span className="text-muted-foreground/60">·</span>
|
||||
<span>{formatRelative(session.updated_at)}</span>
|
||||
</>
|
||||
}
|
||||
onOpen={() => openSession(session)}
|
||||
>
|
||||
<HistoryRowIconButton
|
||||
title="重命名"
|
||||
onClick={() => {
|
||||
setRenameId(session.id);
|
||||
setRenameTitle(session.title);
|
||||
}}
|
||||
>
|
||||
<RenameIcon />
|
||||
</HistoryRowIconButton>
|
||||
<HistoryRowIconButton
|
||||
title="删除"
|
||||
onClick={() => void handleDelete(session)}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</HistoryRowIconButton>
|
||||
</HistoryRow>
|
||||
);
|
||||
})}
|
||||
|
||||
<HistoryRenameDialog
|
||||
open={renameId !== null}
|
||||
initialTitle={renameTitle}
|
||||
dialogTitle="重命名写作"
|
||||
onCancel={() => setRenameId(null)}
|
||||
onConfirm={(title) => renameId && void handleRename(renameId, title)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* 「对话历史」页(/page/workspace/chats)业务历史 Tab(多智能体会商 / AI写作 /
|
||||
* 深度研究)的共享小组件。行样式与「对话」Tab 保持一致:标题行 + 次行元信息 +
|
||||
* 悬浮操作按钮,让四个 Tab 在同一页里观感统一。
|
||||
*/
|
||||
|
||||
import { useEffect, useState, type ReactNode } from "react";
|
||||
import { Pencil, Trash2 } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { isIMEComposing } from "@/lib/ime";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/** 次行状态小标签(对齐侧栏列表的状态药丸配色)。 */
|
||||
export function HistoryStatusPill({
|
||||
label,
|
||||
className,
|
||||
}: {
|
||||
label: string;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<span className={cn("text-[10px] font-medium", className)}>{label}</span>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 单条历史记录行。onOpen 由各 Tab 提供(多数是暂存 sessionStorage 后跳转
|
||||
* 对应工作台,由页面挂载时消费),操作按钮经 children 传入。
|
||||
*/
|
||||
export function HistoryRow({
|
||||
title,
|
||||
meta,
|
||||
onOpen,
|
||||
children,
|
||||
}: {
|
||||
title: ReactNode;
|
||||
meta: ReactNode;
|
||||
onOpen: () => void;
|
||||
children?: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="group/chat-row relative flex flex-col gap-2 border-b">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpen}
|
||||
className="block w-full p-4 pr-24 text-left"
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="truncate">{title}</span>
|
||||
</div>
|
||||
<div className="text-muted-foreground mt-2 flex items-center gap-1 text-sm">
|
||||
{meta}
|
||||
</div>
|
||||
</button>
|
||||
{/* Row actions — kept out of the open button so clicks don't trigger it. */}
|
||||
<div className="absolute right-3 top-3 flex items-center gap-1 opacity-0 transition-opacity group-hover/chat-row:opacity-100 focus-within:opacity-100">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 重命名 / 删除图标操作按钮(与「对话」Tab 的收藏/置顶按钮同规格)。 */
|
||||
export function HistoryRowIconButton({
|
||||
title,
|
||||
onClick,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
onClick: () => void;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8"
|
||||
title={title}
|
||||
aria-label={title}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export function RenameIcon() {
|
||||
return <Pencil className="text-muted-foreground size-4" />;
|
||||
}
|
||||
|
||||
export function DeleteIcon() {
|
||||
return <Trash2 className="text-muted-foreground size-4" />;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自包含重命名弹窗:打开时用 initialTitle 初始化输入框,确认时回调新标题。
|
||||
* 三个 Tab 的重命名接口都是「(id, title) → Promise」,由调用方闭包绑定 id。
|
||||
*/
|
||||
export function HistoryRenameDialog({
|
||||
open,
|
||||
initialTitle,
|
||||
dialogTitle,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
}: {
|
||||
open: boolean;
|
||||
initialTitle: string;
|
||||
dialogTitle: string;
|
||||
onCancel: () => void;
|
||||
onConfirm: (title: string) => void;
|
||||
}) {
|
||||
const [value, setValue] = useState(initialTitle);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) setValue(initialTitle);
|
||||
}, [open, initialTitle]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(o) => (o ? undefined : onCancel())}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{dialogTitle}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
<Input
|
||||
autoFocus
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
placeholder="输入新名称"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && !isIMEComposing(e)) {
|
||||
e.preventDefault();
|
||||
if (value.trim()) onConfirm(value.trim());
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onCancel}>
|
||||
取消
|
||||
</Button>
|
||||
<Button disabled={!value.trim()} onClick={() => onConfirm(value.trim())}>
|
||||
保存
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user