From 36653c53a6099b565f2647b44fdea70d8a5874dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=91=A3=E5=AE=8F=E5=87=AF?= Date: Fri, 28 Aug 2026 13:01:58 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A1=A5=E5=85=85=E5=AF=B9=E8=AF=9D?= =?UTF-8?q?=E5=8E=86=E5=8F=B2=E9=A1=B5=E6=B7=B1=E5=BA=A6=E7=A0=94=E7=A9=B6?= =?UTF-8?q?=20Tab=20=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .../chats-history/ResearchHistoryTab.tsx | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 frontend-web/src/pages/chats-history/ResearchHistoryTab.tsx 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)} + /> + + ); +}