feat: 补充对话历史页深度研究 Tab 组件

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
董宏凯andCursor committed 2026-08-28 13:01:58 +08:00
1 parent 37517c2778
commit 36653c53a6
1 file changed
+173
@@ -0,0 +1,173 @@
"use client";
/**
* 「对话历史」页 —— 深度研究 Tab。
*
* 数据来自 deep-research 会话列表(useResearchSessions)。点击跳转深度研究页并
* 带 ``?session=<id>``,由 DeepResearchWorkbench 挂载时读取该参数直接选中并
* 打开这条研究记录。
*/
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
import {
useResearchActions,
useResearchSessions,
type DeepResearchSession,
} from "@/strategy-components/api/deep-research";
import { formatRelative } from "@/open-canvas/components/ai-writing/AIWritingHistoryDropdown";
import { desensitize } from "@/lib/desensitize";
import {
DeleteIcon,
HistoryRenameDialog,
HistoryRow,
HistoryRowIconButton,
HistoryStatusPill,
RenameIcon,
} from "./history-tab-shared";
const DEEP_RESEARCH_PATH = "/page/workspace/deep-research";
/** 状态文案与配色对齐 DeepResearchWorkbench 的研究记录侧栏。 */
const RESEARCH_STATUS: Record<
DeepResearchSession["status"],
{ label: string; className: string }
> = {
draft: { label: "待开始", className: "text-muted-foreground" },
collecting: { label: "收集中", className: "text-primary" },
awaiting_report: { label: "待撰写", className: "text-amber-500" },
running: { label: "进行中", className: "text-primary" },
awaiting_input: { label: "等待确认", className: "text-amber-500" },
completed: { label: "已完成", className: "text-emerald-600" },
failed: { label: "失败", className: "text-rose-500" },
cancelled: { label: "已取消", className: "text-muted-foreground" },
};
export function ResearchHistoryTab({
query,
onTotalChange,
}: {
query: string;
onTotalChange?: (total: number) => void;
}) {
const navigate = useNavigate();
const { data, isLoading } = useResearchSessions(100);
const actions = useResearchActions();
const [renameId, setRenameId] = useState<string | null>(null);
const [renameTitle, setRenameTitle] = useState("");
const sessions = data?.items ?? [];
const keyword = query.trim().toLowerCase();
const filtered = keyword
? sessions.filter((session) =>
(session.title || session.query).toLowerCase().includes(keyword),
)
: sessions;
useEffect(() => {
onTotalChange?.(filtered.length);
}, [filtered.length, onTotalChange]);
const openSession = (session: DeepResearchSession) => {
navigate(`${DEEP_RESEARCH_PATH}?session=${encodeURIComponent(session.id)}`);
};
const handleRename = async (id: string, title: string) => {
try {
await actions.updateTitle.mutateAsync({ id, title });
toast.success("已重命名");
} catch {
toast.error("重命名失败,请重试");
} finally {
setRenameId(null);
}
};
const handleDelete = async (session: DeepResearchSession) => {
if (!window.confirm("确定删除这条研究记录吗?此操作不可恢复。")) return;
try {
await actions.delete.mutateAsync(session.id);
toast.success("已删除研究记录");
} catch (error) {
toast.error(error instanceof Error ? error.message : "删除失败,请重试");
}
};
if (isLoading && 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 status = RESEARCH_STATUS[session.status] ?? {
label: session.status,
className: "text-muted-foreground",
};
return (
<HistoryRow
key={session.id}
title={desensitize(session.title || session.query || "未命名研究")}
meta={
<>
<HistoryStatusPill
label={status.label}
className={status.className}
/>
<span className="text-muted-foreground/60">·</span>
{session.sourceCount > 0 && (
<>
<span>{session.sourceCount} 条来源</span>
<span className="text-muted-foreground/60">·</span>
</>
)}
<span>{formatRelative(session.updatedAt ?? session.createdAt ?? "")}</span>
</>
}
onOpen={() => openSession(session)}
>
<HistoryRowIconButton
title="重命名"
onClick={() => {
setRenameId(session.id);
setRenameTitle(session.title || session.query || "");
}}
>
<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)}
/>
</>
);
}