From 875001a8c08345f9a1223ada4eb8c935b681a127 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 15:43:02 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AF=B9=E8=AF=9D=E5=8E=86=E5=8F=B2?= =?UTF-8?q?=E9=A1=B5=E6=94=B6=E7=BA=B3=E4=BC=9A=E5=95=86/=E5=86=99?= =?UTF-8?q?=E4=BD=9C/=E6=B7=B1=E5=BA=A6=E7=A0=94=E7=A9=B6=20Tab=EF=BC=8C?= =?UTF-8?q?=E4=BE=A7=E6=A0=8F=E7=A7=BB=E9=99=A4=E9=87=8D=E5=A4=8D=E5=85=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ChatsPage 新增四类历史 Tab 与统一搜索筛选 UI;简洁模式侧栏不再展示会商与写作列表;深度研究页支持 ?session= 深链打开指定记录。 Co-authored-by: Cursor --- .../workspace/recent-roundtable-list.tsx | 267 --------- .../workspace/recent-writing-list.tsx | 122 ---- .../workspace/workspace-sidebar.tsx | 68 ++- frontend-web/src/pages/ChatsPage.tsx | 526 ++++++++++++++---- .../chats-history/ResearchHistoryTab.tsx | 98 ++-- .../chats-history/RoundtableHistoryTab.tsx | 82 +-- .../pages/chats-history/WritingHistoryTab.tsx | 75 +-- .../chats-history/history-tab-shared.tsx | 35 +- .../deep-research/DeepResearchWorkbench.tsx | 8 +- 9 files changed, 612 insertions(+), 669 deletions(-) delete mode 100644 frontend-web/src/components/workspace/recent-roundtable-list.tsx delete mode 100644 frontend-web/src/components/workspace/recent-writing-list.tsx diff --git a/frontend-web/src/components/workspace/recent-roundtable-list.tsx b/frontend-web/src/components/workspace/recent-roundtable-list.tsx deleted file mode 100644 index 8494a9a0..00000000 --- a/frontend-web/src/components/workspace/recent-roundtable-list.tsx +++ /dev/null @@ -1,267 +0,0 @@ -"use client"; - -import { useCallback, useEffect, useState } from "react"; -import { usePathname, useRouter } from "next/navigation"; -import { ChevronDown, Loader2, MoreHorizontal, Pencil, Share2, Trash2 } from "lucide-react"; -import { toast } from "sonner"; - -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { Input } from "@/components/ui/input"; -import { - SidebarGroup, - SidebarGroupContent, - SidebarGroupLabel, - SidebarMenu, - SidebarMenuAction, - SidebarMenuItem, - SidebarMenuButton, -} from "@/components/ui/sidebar"; -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 { isIMEComposing } from "@/lib/ime"; - -const ROUNDTABLE_PATH = "/page/workspace/roundtable/planning"; - -function openDraft( - draft: DraftMeta, - isOnPage: boolean, - router: ReturnType, -) { - if (isOnPage) { - // 已在会商页面:直接派事件让页面加载该草稿(仿 AI 写作的 open-session)。 - window.dispatchEvent( - new CustomEvent("roundtable:open-draft", { detail: draft.id }), - ); - } else { - // 不在该页面:把草稿 id 暂存到 sessionStorage,跳转后由页面消费。 - sessionStorage.setItem("roundtable:pending-draft", draft.id); - router.push(ROUNDTABLE_PATH); - } -} - -/** 把草稿的后台作业状态 / 步骤映射成侧边栏小标签(仿页面内 HistoryDropdown)。 */ -function StatusBadge({ draft }: { draft: DraftMeta }) { - if (draft.status === "running") { - return ( - - - 进行中 - - ); - } - if (draft.status === "awaiting_input") { - return 待澄清; - } - if (draft.status === "error") { - return 出错; - } - // 非进行中/待澄清/出错时,展示本次会商所用业务链(无链条 → 自由模式)。 - return ( - - {draft.chainTitle || "自由模式"} - - ); -} - -export function RecentRoundtableList() { - const router = useRouter(); - const pathname = usePathname(); - const { drafts, loading, refresh, rename, remove } = useRoundtableDrafts(); - const [collapsed, setCollapsed] = useState( - () => localStorage.getItem("sidebar:recent-roundtable-collapsed") === "true", - ); - - // 重命名对话框状态。 - const [renameId, setRenameId] = useState(null); - const [renameValue, setRenameValue] = useState(""); - // 分享对话框 —— 正在分享的草稿 id。 - const [shareId, setShareId] = useState(null); - - const toggleCollapsed = () => { - const next = !collapsed; - setCollapsed(next); - localStorage.setItem("sidebar:recent-roundtable-collapsed", String(next)); - }; - - useEffect(() => { - void refresh(); - }, [refresh]); - - const handleRenameSubmit = useCallback(async () => { - const id = renameId; - const title = renameValue.trim(); - if (!id || !title) { - setRenameId(null); - return; - } - try { - await rename(id, title); - toast.success("已重命名"); - } catch { - toast.error("重命名失败,请重试"); - } finally { - setRenameId(null); - setRenameValue(""); - } - }, [renameId, renameValue, rename]); - - const handleDelete = useCallback( - async (draft: DraftMeta) => { - if (!window.confirm("确定删除这条会商记录吗?此操作不可恢复。")) return; - try { - await remove(draft.id); - toast.success("已删除会商记录"); - } catch { - toast.error("删除失败,请重试"); - } - }, - [remove], - ); - - if (!loading && drafts.length === 0) return null; - - const isOnPage = pathname === ROUNDTABLE_PATH; - - return ( - <> - - - 多智能体会商 - - - - -
- {loading && drafts.length === 0 ? ( -

加载中…

- ) : ( - drafts.slice(0, 10).map((draft) => ( - - openDraft(draft, isOnPage, router)} - > -
- - {draft.title || "未命名草稿"} - -
- - · - - {formatRelative(draft.updatedAt)} - -
-
-
- - - - - 更多 - - - - { - setRenameId(draft.id); - setRenameValue(draft.title || ""); - }} - > - - 重命名 - - setShareId(draft.id)}> - - 分享 - - - void handleDelete(draft)}> - - 删除 - - - -
- )) - )} -
-
-
-
- - {/* 重命名对话框 */} - !o && setRenameId(null)}> - - - 重命名会商 - -
- setRenameValue(e.target.value)} - placeholder="输入新名称" - onKeyDown={(e) => { - if (e.key === "Enter" && !isIMEComposing(e)) { - e.preventDefault(); - void handleRenameSubmit(); - } - }} - /> -
- - - - -
-
- - {/* 分享对话框 */} - { - if (!o) setShareId(null); - }} - /> - - ); -} diff --git a/frontend-web/src/components/workspace/recent-writing-list.tsx b/frontend-web/src/components/workspace/recent-writing-list.tsx deleted file mode 100644 index 11be8295..00000000 --- a/frontend-web/src/components/workspace/recent-writing-list.tsx +++ /dev/null @@ -1,122 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { usePathname, useRouter } from "next/navigation"; -import { ChevronDown } from "lucide-react"; -import { cn } from "@/lib/utils"; -import { - SidebarGroup, - SidebarGroupContent, - SidebarGroupLabel, - SidebarMenu, - SidebarMenuItem, - SidebarMenuButton, -} from "@/components/ui/sidebar"; -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"; - -const AI_WRITING_PATH = "/page/workspace/ai-writing"; - -function openSession( - session: AIWritingSessionSummary, - isOnPage: boolean, - router: ReturnType, -) { - if (isOnPage) { - // 已在 AI 写作页面,直接通过自定义事件通知面板 - window.dispatchEvent( - new CustomEvent("ai-writing:open-session", { detail: session }), - ); - } else { - // 不在该页面,把 session 存入 sessionStorage 后跳转 - sessionStorage.setItem("ai-writing:pending-session", JSON.stringify(session)); - router.push(AI_WRITING_PATH); - } -} - -export function RecentWritingList() { - const router = useRouter(); - const pathname = usePathname(); - const { sessions, loading, refresh } = useAIWritingSessions(); - const [collapsed, setCollapsed] = useState( - () => localStorage.getItem("sidebar:recent-writing-collapsed") === "true", - ); - - const toggleCollapsed = () => { - const next = !collapsed; - setCollapsed(next); - localStorage.setItem("sidebar:recent-writing-collapsed", String(next)); - }; - - useEffect(() => { - void refresh(); - }, [refresh]); - - if (!loading && sessions.length === 0) return null; - - const isOnPage = pathname === AI_WRITING_PATH; - - return ( - - - 最近写作 - - - - -
- {loading && sessions.length === 0 ? ( -

加载中…

- ) : ( - sessions.slice(0, 10).map((session) => { - const { label, color } = STATUS_LABELS[session.status] ?? { - label: session.status, - color: "text-muted-foreground", - }; - return ( - - openSession(session, isOnPage, router)} - > -
- - {session.title} - -
- - {label} - - · - - {formatRelative(session.updated_at)} - -
-
-
-
- ); - }) - )} -
-
-
-
- ); -} diff --git a/frontend-web/src/components/workspace/workspace-sidebar.tsx b/frontend-web/src/components/workspace/workspace-sidebar.tsx index 331b97b9..7614b40b 100644 --- a/frontend-web/src/components/workspace/workspace-sidebar.tsx +++ b/frontend-web/src/components/workspace/workspace-sidebar.tsx @@ -14,21 +14,19 @@ import { usePathname } from "next/navigation"; import { GeneralQaSidebar } from "./general-qa-sidebar"; import { RecentChatList } from "./recent-chat-list"; -import { RecentRoundtableList } from "./recent-roundtable-list"; -import { RecentWritingList } from "./recent-writing-list"; import { WorkspaceHeader } from "./workspace-header"; import { WorkspaceNavChatList } from "./workspace-nav-chat-list"; import { WorkspaceNavMenu } from "./workspace-nav-menu"; -export function WorkspaceSidebar({ - compactChrome = false, - forceCollapsed = false, - className, - ...props -}: React.ComponentProps & { - compactChrome?: boolean; - forceCollapsed?: boolean; -}) { +export function WorkspaceSidebar({ + compactChrome = false, + forceCollapsed = false, + className, + ...props +}: React.ComponentProps & { + compactChrome?: boolean; + forceCollapsed?: boolean; +}) { const { open: isSidebarOpen } = useSidebar(); const { theme } = useTheme(); const pathname = usePathname(); @@ -36,26 +34,26 @@ export function WorkspaceSidebar({ // 通用问答(全量模式)左栏:新建回答 → 常用智能体 → 聊天记录(需求 0616 #1)。 // 仅在通用问答(/page/workspace/chats*)下启用三块式布局;点击常用智能体后是 // 「原地嵌入」(停留在 /chats/new?agent=...,仅右侧问答区换成智能体对话),左侧栏不变。 - const isGeneralQa = pathname.startsWith("/page/workspace/chats"); - - if (forceCollapsed) { - return ( - - - - - - ); - } - + const isGeneralQa = pathname.startsWith("/page/workspace/chats"); + + if (forceCollapsed) { + return ( + + + + + + ); + } + // 紧凑模式:用「外层定宽容器 + transition-[width]」做展开/收起过渡动画。 // collapsible="none" 的 Sidebar 是普通 in-flow 定宽块(非 absolute,故不受本布局对 // sidebar-container 的 absolute/left-0 覆盖影响),外层 overflow-hidden 把它从 @@ -123,10 +121,10 @@ export function WorkspaceSidebar({ compact={compactChrome} /> {isSidebarOpen && } - {/* 「多智能体会商」「最近写作」与「智能体/技能」等菜单项保持一致的显隐: - 仅在非紧凑模式(!compactChrome)下展示,紧凑模式(有外层页面侧栏时)一并隐藏。 */} - {isSidebarOpen && !compactChrome && } - {isSidebarOpen && !compactChrome && } + {/* 「多智能体会商」「最近写作」历史入口已移至「对话历史」页 + (/page/workspace/chats 的 Tabs,2026-08 需求):简洁模式(本分支, + 无全局导航栏/页面侧栏时的工作区侧栏)不再在左下角展示这两个分组, + 全量模式本就不展示。 */} {!compactChrome && ( diff --git a/frontend-web/src/pages/ChatsPage.tsx b/frontend-web/src/pages/ChatsPage.tsx index b8ac2d0e..d6a8e4d5 100644 --- a/frontend-web/src/pages/ChatsPage.tsx +++ b/frontend-web/src/pages/ChatsPage.tsx @@ -1,18 +1,24 @@ "use client"; import { + Bot, ChevronLeft, ChevronRight, + Clock, DownloadIcon, + FilePenLine, + MessageSquare, Pin, PinOff, + Search, Star, + type LucideIcon, + X, } from "lucide-react"; import Link from "next/link"; -import { useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scroll-area"; import { ImportConversationsDialog } from "@/components/workspace/thread-share-dialogs"; import { @@ -43,8 +49,66 @@ import { formatTimeAgo } from "@/core/utils/datetime"; import { desensitize } from "@/lib/desensitize"; import { cn } from "@/lib/utils"; +import { RoundtableHistoryTab } from "./chats-history/RoundtableHistoryTab"; +import { WritingHistoryTab } from "./chats-history/WritingHistoryTab"; +import { ResearchHistoryTab } from "./chats-history/ResearchHistoryTab"; + const PAGE_SIZE = 20; +/** + * 「对话历史」页的四个历史 Tab:对话(通用问答/智能体会话)之外,把多智能体 + * 会商、AI写作、深度研究的记录也收纳进来(简洁模式侧栏不再展示后两者的入口, + * 统一从这里查看)。 + */ +type HistoryTab = "chats" | "roundtable" | "writing" | "research"; +type ChatFilter = "all" | "favorites" | "pinned"; + +const HISTORY_TABS: { + id: HistoryTab; + label: string; + description: string; + icon: LucideIcon; +}[] = [ + { + id: "chats", + label: "对话", + description: "普通问答与智能体会话", + icon: MessageSquare, + }, + { + id: "roundtable", + label: "多智能体会商", + description: "多角色协作研讨", + icon: Bot, + }, + { + id: "writing", + label: "AI写作", + description: "写作任务与草稿", + icon: FilePenLine, + }, + { + id: "research", + label: "深度研究", + description: "研究任务与报告", + icon: Search, + }, +]; + +/** 非对话 Tab 的搜索占位文案(对话 Tab 沿用 i18n 的 t.chats.searchChats)。 */ +const TAB_SEARCH_PLACEHOLDER: Record, string> = { + roundtable: "搜索会商记录…", + writing: "搜索写作记录…", + research: "搜索研究记录…", +}; + +const TAB_TOTAL_UNIT: Record = { + chats: "条对话", + roundtable: "条会商记录", + writing: "篇写作", + research: "项研究", +}; + export default function ChatsPage({ compactChrome = false, }: { @@ -54,11 +118,14 @@ export default function ChatsPage({ const { t } = useI18n(); const { toggle: togglePin } = useToggleThreadPin(); const { toggle: toggleFavorite } = useToggleThreadFavorite(); + const [activeTab, setActiveTab] = useState("chats"); const [search, setSearch] = useState(""); const [debouncedSearch, setDebouncedSearch] = useState(""); - const [onlyFavorites, setOnlyFavorites] = useState(false); + const [chatFilter, setChatFilter] = useState("all"); const [page, setPage] = useState(1); const [importOpen, setImportOpen] = useState(false); + // 非对话 Tab 的记录总数由 Tab 组件回传(客户端过滤后的条数)。 + const [tabTotal, setTabTotal] = useState(null); useEffect(() => { document.title = `${t.pages.chats} - ${t.pages.appName}`; @@ -73,26 +140,41 @@ export default function ChatsPage({ // Reset to the first page whenever the filtered result set changes. useEffect(() => { setPage(1); - }, [debouncedSearch, onlyFavorites]); + }, [debouncedSearch, chatFilter]); + + // 切换 Tab 时回到第一页并清空旧 Tab 上报的计数。 + useEffect(() => { + setPage(1); + setTabTotal(null); + }, [activeTab]); + + const handleTabTotalChange = useCallback((total: number) => { + setTabTotal(total); + }, []); // Conversations are paginated on the backend: each page is one bounded // query (title search + favorites filter included), so a long history is // never fetched in one go. Pinned conversations come from their own small // query and are surfaced in a dedicated section on the first page only, // and dropped from the paginated rows so they don't repeat on every page. - const favoriteFilter = useMemo( - () => (onlyFavorites ? { is_favorite: true } : {}), - [onlyFavorites], + const threadFilter = useMemo( + () => + chatFilter === "favorites" + ? { is_favorite: true } + : chatFilter === "pinned" + ? { is_pinned: true } + : {}, + [chatFilter], ); const pinnedFilter = useMemo( - () => ({ ...favoriteFilter, is_pinned: true }), - [favoriteFilter], + () => ({ ...threadFilter, is_pinned: true }), + [threadFilter], ); const { data: pageData } = useThreadsPage({ page, pageSize: PAGE_SIZE, search: debouncedSearch, - metadata: favoriteFilter, + metadata: threadFilter, }); const { data: pinnedData } = useThreadsPage({ page: 1, @@ -102,13 +184,28 @@ export default function ChatsPage({ }); const { data: totalCount } = useThreadsCount({ search: debouncedSearch, - metadata: favoriteFilter, + metadata: threadFilter, + }); + const { data: allThreadsCount } = useThreadsCount({ + search: debouncedSearch, + metadata: {}, + }); + const { data: favoriteThreadsCount } = useThreadsCount({ + search: debouncedSearch, + metadata: { is_favorite: true }, + }); + const { data: pinnedThreadsCount } = useThreadsCount({ + search: debouncedSearch, + metadata: { is_pinned: true }, }); - const pinnedThreads = pinnedData?.threads ?? []; + const pinnedThreads = chatFilter === "pinned" ? [] : (pinnedData?.threads ?? []); const pagedThreads = useMemo( - () => (pageData?.threads ?? []).filter((t) => !isThreadPinned(t)), - [pageData?.threads], + () => + (pageData?.threads ?? []).filter( + (thread) => chatFilter === "pinned" || !isThreadPinned(thread), + ), + [chatFilter, pageData?.threads], ); const currentPage = page; const hasMore = pageData?.hasMore ?? false; @@ -123,46 +220,67 @@ export default function ChatsPage({ return (
- -
+ + +
{pinned && ( - + + + 置顶 + )} - + + + 对话 + + {desensitize(titleOfThread(thread, t.pages.newChat))}
{thread.updated_at && ( -
+
+ {formatTimeAgo(thread.updated_at)}
)} - {/* Row actions — kept out of the Link so clicks don't navigate. */} -
+ {thread.updated_at && ( +
+ + {formatTimeAgo(thread.updated_at)} +
+ )} +
- - -
- -
- setSearch(e.target.value)} - /> - {totalCount != null && ( -
- 共 {totalCount} 条对话 + {HISTORY_TABS.map((tab) => { + const isActive = activeTab === tab.id; + const Icon = tab.icon; + + return ( + + ); + })}
- )} + {activeTab === "chats" && ( + + )} +
- -
- {currentPage === 1 && pinnedThreads.length > 0 && ( - <> -
- {t.chats.pinnedSection} + +
+
+
+
+ {activeTab === "chats" ? ( +
+ + + {pinnedThreadsCount != null && pinnedThreadsCount > 0 && ( + + )} +
+ ) : ( +
+ {activeTabConfig.description} +
+ )} + +
+
+ {displayTotal != null && ( + + 共 {displayTotal}{" "} + {TAB_TOTAL_UNIT[activeTab]} + + )}
- {pinnedThreads.map(renderThread)} - - )} - {pagedThreads.map(renderThread)} - {isEmpty && ( -
- {onlyFavorites ? t.chats.noFavorites : t.chats.noChats}
- )} - {(currentPage > 1 || hasMore) && ( -
- - - {totalPages != null - ? `${currentPage} / ${totalPages}` - : currentPage} - - -
- )} + {activeTab === "chats" && ( +
+ {currentPage === 1 && pinnedThreads.map(renderThread)} + {pagedThreads.map(renderThread)} + {isEmpty && ( +
+ {chatFilter === "favorites" + ? t.chats.noFavorites + : chatFilter === "pinned" + ? "暂无置顶对话" + : t.chats.noChats} +
+ )} + {(currentPage > 1 || hasMore) && ( +
+ + + {totalPages != null + ? `${currentPage} / ${totalPages}` + : currentPage} + + +
+ )} +
+ )} + {activeTab === "roundtable" && ( +
+ +
+ )} + {activeTab === "writing" && ( +
+ +
+ )} + {activeTab === "research" && ( +
+ +
+ )} +
diff --git a/frontend-web/src/pages/chats-history/ResearchHistoryTab.tsx b/frontend-web/src/pages/chats-history/ResearchHistoryTab.tsx index 5d89b688..41608b03 100644 --- a/frontend-web/src/pages/chats-history/ResearchHistoryTab.tsx +++ b/frontend-web/src/pages/chats-history/ResearchHistoryTab.tsx @@ -10,6 +10,7 @@ import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; +import { Search } from "lucide-react"; import { toast } from "sonner"; import { @@ -26,6 +27,7 @@ import { HistoryRow, HistoryRowIconButton, HistoryStatusPill, + HistoryTypeBadge, RenameIcon, } from "./history-tab-shared"; @@ -115,51 +117,59 @@ export function ResearchHistoryTab({ 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 || ""); - }} +
+ {filtered.map((session) => { + const status = RESEARCH_STATUS[session.status] ?? { + label: session.status, + className: "text-muted-foreground", + }; + const timestamp = session.updatedAt ?? session.createdAt ?? null; + return ( + } + title={desensitize(session.title || session.query || "未命名研究")} + meta={ + <> + + {session.sourceCount > 0 && ( + <> + · + {session.sourceCount} 条来源 + + )} + {timestamp && ( + <> + · + {formatRelative(timestamp)} + + )} + + } + onOpen={() => openSession(session)} > - - - void handleDelete(session)} - > - - - - ); - })} + { + setRenameId(session.id); + setRenameTitle(session.title || session.query || ""); + }} + > + + + void handleDelete(session)} + > + + + + ); + })} +
- {filtered.map((draft) => { - const status = statusOf(draft); - return ( - - - · - {formatRelative(draft.updatedAt)} - - } - onOpen={() => openDraft(draft)} - > - { - setRenameId(draft.id); - setRenameTitle(draft.title || ""); - }} +
+ {filtered.map((draft) => { + const status = statusOf(draft); + return ( + } + title={desensitize(draft.title || "未命名草稿")} + meta={ + <> + + · + {formatRelative(draft.updatedAt)} + + } + onOpen={() => openDraft(draft)} > - - - setShareId(draft.id)}> - - - void handleDelete(draft)} - > - - - - ); - })} + { + setRenameId(draft.id); + setRenameTitle(draft.title || ""); + }} + > + + + setShareId(draft.id)}> + + + void handleDelete(draft)} + > + + + + ); + })} +
- {filtered.map((session) => { - const { label, color } = STATUS_LABELS[session.status] ?? { - label: session.status, - color: "text-muted-foreground", - }; - return ( - - - · - {formatRelative(session.updated_at)} - - } - onOpen={() => openSession(session)} - > - { - setRenameId(session.id); - setRenameTitle(session.title); - }} +
+ {filtered.map((session) => { + const { label, color } = STATUS_LABELS[session.status] ?? { + label: session.status, + color: "text-muted-foreground", + }; + return ( + } + title={desensitize(session.title)} + meta={ + <> + + · + {formatRelative(session.updated_at)} + + } + onOpen={() => openSession(session)} > - - - void handleDelete(session)} - > - - - - ); - })} + { + setRenameId(session.id); + setRenameTitle(session.title); + }} + > + + + void handleDelete(session)} + > + + + + ); + })} +
+ + {label} + + ); +} + /** * 单条历史记录行。onOpen 由各 Tab 提供(多数是暂存 sessionStorage 后跳转 * 对应工作台,由页面挂载时消费),操作按钮经 children 传入。 */ export function HistoryRow({ + badge, title, meta, onOpen, children, }: { + badge?: ReactNode; title: ReactNode; meta: ReactNode; onOpen: () => void; children?: ReactNode; }) { return ( -
+
{/* Row actions — kept out of the open button so clicks don't trigger it. */} -
+
{children}
diff --git a/frontend-web/src/pages/deep-research/DeepResearchWorkbench.tsx b/frontend-web/src/pages/deep-research/DeepResearchWorkbench.tsx index 18fdf1b1..cf073925 100644 --- a/frontend-web/src/pages/deep-research/DeepResearchWorkbench.tsx +++ b/frontend-web/src/pages/deep-research/DeepResearchWorkbench.tsx @@ -20,6 +20,7 @@ */ import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useSearchParams } from "react-router-dom"; import type { Message } from "@langchain/langgraph-sdk"; import ReactMarkdown from "react-markdown"; import { Select as TSelect } from "tdesign-react"; @@ -444,7 +445,12 @@ export default function DeepResearchWorkbench() { } function DeepResearchWorkbenchInner() { - const [selectedId, setSelectedId] = useState(null); + // 「?session=」深链:对话历史页「深度研究」Tab 等外部入口直接定位到某条 + // 研究记录。仅作初始选中(首帧读取一次);之后的切换仍走页内状态,不回写 URL。 + const [searchParams] = useSearchParams(); + const [selectedId, setSelectedId] = useState(() => + searchParams.get("session"), + ); const [mode, setMode] = useState("basic"); const [generateImages, setGenerateImages] = useState(false); const [advancedOptions, setAdvancedOptions] = useState(DEFAULT_ADVANCED_OPTIONS);