diff --git a/frontend-web/src/pages/chats-history/ResearchHistoryTab.tsx b/frontend-web/src/pages/chats-history/ResearchHistoryTab.tsx new file mode 100644 index 00000000..5d89b688 --- /dev/null +++ b/frontend-web/src/pages/chats-history/ResearchHistoryTab.tsx @@ -0,0 +1,173 @@ +"use client"; + +/** + * 「对话历史」页 —— 深度研究 Tab。 + * + * 数据来自 deep-research 会话列表(useResearchSessions)。点击跳转深度研究页并 + * 带 ``?session=``,由 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(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 ( +
+ 加载中… +
+ ); + } + + if (filtered.length === 0) { + return ( +
+ {keyword ? "没有匹配的研究记录" : "暂无研究记录"} +
+ ); + } + + return ( + <> + {filtered.map((session) => { + const status = RESEARCH_STATUS[session.status] ?? { + label: session.status, + className: "text-muted-foreground", + }; + return ( + + + · + {session.sourceCount > 0 && ( + <> + {session.sourceCount} 条来源 + · + + )} + {formatRelative(session.updatedAt ?? session.createdAt ?? "")} + + } + onOpen={() => openSession(session)} + > + { + setRenameId(session.id); + setRenameTitle(session.title || session.query || ""); + }} + > + + + void handleDelete(session)} + > + + + + ); + })} + + setRenameId(null)} + onConfirm={(title) => renameId && void handleRename(renameId, title)} + /> + + ); +}