diff --git a/frontend-web/src/components/workspace/agents/agent-chat-sidebar.tsx b/frontend-web/src/components/workspace/agents/agent-chat-sidebar.tsx
index 03fddad8..6863e4c4 100644
--- a/frontend-web/src/components/workspace/agents/agent-chat-sidebar.tsx
+++ b/frontend-web/src/components/workspace/agents/agent-chat-sidebar.tsx
@@ -1,10 +1,46 @@
"use client";
-import { Bot, Loader2, Search, X } from "lucide-react";
+import {
+ Bot,
+ Download,
+ FileJson,
+ FileText,
+ Loader2,
+ MessageSquare,
+ MoreHorizontal,
+ Pencil,
+ Pin,
+ PinOff,
+ Search,
+ Share2,
+ Star,
+ Trash2,
+ X,
+} from "lucide-react";
import Link from "next/link";
-import { useEffect, useMemo, useState } from "react";
+import { useParams, usePathname, useRouter } from "next/navigation";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { toast } from "sonner";
import { agentColor } from "@/components/workspace/agent-icon-color";
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import {
Select,
@@ -13,11 +49,35 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
+import { ShareConversationDialog } from "@/components/workspace/thread-share-dialogs";
import { useTheme } from "@/contexts/ThemeContext";
import { useAgentsPaginated } from "@/core/agents";
+import { getAPIClient } from "@/core/api";
+import { useI18n } from "@/core/i18n/hooks";
import { useRouteBase } from "@/core/route-base";
import { usePublishSquares } from "@/core/system-settings/hooks";
+import {
+ exportThreadAsJSON,
+ exportThreadAsMarkdown,
+} from "@/core/threads/export";
+import {
+ useDeleteThread,
+ useRenameThread,
+ useThreads,
+ useToggleThreadFavorite,
+ useToggleThreadPin,
+} from "@/core/threads/hooks";
+import type { AgentThread, AgentThreadState } from "@/core/threads/types";
+import {
+ isThreadFavorite,
+ isThreadPinned,
+ sortThreadsPinnedFirst,
+ titleOfThread,
+ usePathOfThread,
+} from "@/core/threads/utils";
+import { env } from "@/env";
import { desensitize } from "@/lib/desensitize";
+import { isIMEComposing } from "@/lib/ime";
import { cn } from "@/lib/utils";
/** 「我的」作用域的下拉选项值;其余选项值为各发布广场(publish square)的 id。 */
@@ -27,18 +87,23 @@ const MINE_SCOPE = "mine";
// 避免静默截断。侧边栏只做快速切换,不在此分页。
const LIST_PAGE_SIZE = 50;
+// 「最近的对话」未搜索时最多展示条数(与首页「通用问答」左栏一致)。
+const MAX_RECENT_CHATS = 10;
+
/**
- * 智能体问答页左侧「智能体列表」栏。
+ * 智能体问答页左侧「智能体列表 + 最近对话」栏(参考首页通用问答左栏)。
*
- * - 顶部下拉:选「我的」(默认,= 我创建/收藏的)或任一「广场」(发布广场 publish square),
- * 与「智能体管理」页的主标签一致。
- * - 下方:所选作用域下的智能体列表;点击进入该智能体的新对话(保持当前路由命名空间)。
- * - 当前正在对话的智能体高亮。
+ * 自上而下:
+ * 1. 顶部下拉:选「我的」(默认,= 我创建/收藏的)或任一「广场」(发布广场),
+ * 与「智能体管理」页主标签一致;下方为智能体名称模糊搜索框。
+ * 2. 智能体列表:所选作用域下的智能体;点击进入该智能体的新对话。当前智能体高亮。
+ * 3. 最近的对话:跨智能体的近期会话(参考首页),点击打开;支持重命名/置顶/收藏/
+ * 分享/导出/删除。当前会话高亮。
*
- * 仅在独立的智能体问答整页(/page/strategy|workspace/agents/:id/chats/:thread)展示;
- * 任务工作区 / zzJC·zzXD 模块 / 嵌入态不展示(它们各有自己的左栏)。
+ * 仅在独立的智能体问答整页展示;任务工作区 / zzJC·zzXD 模块 / 嵌入态不展示。
*/
export function AgentChatSidebar({ activeAgentId }: { activeAgentId?: string }) {
+ const { t } = useI18n();
const routeBase = useRouteBase();
const { theme } = useTheme();
const isDark = theme === "dark";
@@ -124,8 +189,11 @@ export function AgentChatSidebar({ activeAgentId }: { activeAgentId?: string })
- {/* 智能体列表 */}
-
+ {/* 智能体列表(上半,独立滚动) */}
+
+
+ 智能体
+
{isLoading && agents.length === 0 ? (
@@ -186,19 +254,325 @@ export function AgentChatSidebar({ activeAgentId }: { activeAgentId?: string })
);
})}
+ {/* 超出一页时引导到完整的智能体管理页(不在侧边栏静默截断) */}
+ {total > agents.length && (
+
+ 共 {total} 个,查看全部 →
+
+ )}
)}
- {/* 底部:超出一页时引导到完整的智能体管理页(不在侧边栏静默截断) */}
- {total > agents.length && (
-
- 共 {total} 个,查看全部 →
-
- )}
+ {/* 最近的对话(下半,独立滚动)—— 参考首页通用问答左栏 */}
+
);
}
+
+/**
+ * 「最近的对话」面板(参考首页 GeneralQaSidebar 的「最近的对话」区块)。
+ * 跨智能体的近期会话,点击打开;支持重命名/置顶/收藏/分享/导出/删除。
+ */
+function RecentChatsPane({ activeAgentId }: { activeAgentId?: string }) {
+ const { t } = useI18n();
+ const pathname = usePathname();
+ const router = useRouter();
+ const pathOf = usePathOfThread();
+
+ const { thread_id: threadIdFromPath } = useParams<{ thread_id: string }>();
+ const { data: threads = [] } = useThreads();
+ const { mutate: deleteThread } = useDeleteThread();
+ const { mutate: renameThread } = useRenameThread();
+ const { toggle: togglePin } = useToggleThreadPin();
+ const { toggle: toggleFavorite } = useToggleThreadFavorite();
+
+ const [query, setQuery] = useState("");
+ const [renameDialogOpen, setRenameDialogOpen] = useState(false);
+ const [renameThreadId, setRenameThreadId] = useState
(null);
+ const [renameValue, setRenameValue] = useState("");
+ const [shareThreadId, setShareThreadId] = useState(null);
+
+ // 排除调度/引导/会商类会话,与首页一致;置顶优先。
+ const visibleThreads = useMemo(
+ () =>
+ sortThreadsPinnedFirst(
+ threads.filter(
+ (thread) =>
+ thread.metadata?.thread_type !== "scheduler" &&
+ thread.metadata?.thread_type !== "bootstrap" &&
+ thread.metadata?.thread_type !== "roundtable",
+ ),
+ ),
+ [threads],
+ );
+
+ const filteredThreads = useMemo(() => {
+ const q = query.trim().toLowerCase();
+ const list = q
+ ? visibleThreads.filter((thread) =>
+ titleOfThread(thread, t.pages.newChat).toLowerCase().includes(q),
+ )
+ : visibleThreads;
+ return q ? list : list.slice(0, MAX_RECENT_CHATS);
+ }, [visibleThreads, query, t.pages.newChat]);
+
+ const handleDelete = useCallback(
+ (threadId: string) => {
+ deleteThread({ threadId });
+ if (threadId === threadIdFromPath) {
+ const threadIndex = threads.findIndex((th) => th.thread_id === threadId);
+ let nextThreadPath = pathOf("new", { agent_id: activeAgentId });
+ if (threadIndex > -1) {
+ if (threads[threadIndex + 1]) {
+ nextThreadPath = pathOf(threads[threadIndex + 1]!);
+ } else if (threads[threadIndex - 1]) {
+ nextThreadPath = pathOf(threads[threadIndex - 1]!);
+ }
+ }
+ void router.push(nextThreadPath);
+ }
+ },
+ [activeAgentId, deleteThread, pathOf, router, threadIdFromPath, threads],
+ );
+
+ const handleRenameClick = useCallback(
+ (threadId: string, currentTitle: string) => {
+ setRenameThreadId(threadId);
+ setRenameValue(currentTitle);
+ setRenameDialogOpen(true);
+ },
+ [],
+ );
+
+ const handleRenameSubmit = useCallback(() => {
+ if (renameThreadId && renameValue.trim()) {
+ renameThread({ threadId: renameThreadId, title: renameValue.trim() });
+ setRenameDialogOpen(false);
+ setRenameThreadId(null);
+ setRenameValue("");
+ }
+ }, [renameThread, renameThreadId, renameValue]);
+
+ const handleExport = useCallback(
+ async (thread: AgentThread, format: "markdown" | "json") => {
+ try {
+ const apiClient = getAPIClient();
+ const state = await apiClient.threads.getState(
+ thread.thread_id,
+ );
+ const messages = state.values?.messages ?? [];
+ if (messages.length === 0) {
+ toast.error(t.conversation.noMessages);
+ return;
+ }
+ if (format === "markdown") exportThreadAsMarkdown(thread, messages);
+ else exportThreadAsJSON(thread, messages);
+ toast.success(t.common.exportSuccess);
+ } catch {
+ toast.error("Failed to export conversation");
+ }
+ },
+ [t],
+ );
+
+ const isStatic = env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true";
+
+ return (
+
+
+ {t.sidebar.recentChats}
+
+ {visibleThreads.length}
+
+
+
+ {/* 历史会话搜索框 */}
+
+
+ setQuery(e.target.value)}
+ placeholder="搜索历史会话…"
+ className="h-8 rounded-lg border-border bg-card pl-8 text-[12px]"
+ />
+
+
+
+ {filteredThreads.length === 0 ? (
+
+ {query ? "没有匹配的会话" : t.chats.noChats}
+
+ ) : (
+ filteredThreads.map((thread) => {
+ const href = pathOf(thread);
+ const isActive = href === pathname;
+ const pinned = isThreadPinned(thread);
+ const favorited = isThreadFavorite(thread);
+ return (
+
+
+
+ {pinned && (
+
+ )}
+
+ {desensitize(titleOfThread(thread, t.pages.newChat))}
+
+ {isActive && (
+
+ )}
+
+ {!isStatic && (
+
+
+
+
+
+
+ handleRenameClick(
+ thread.thread_id,
+ titleOfThread(thread, t.pages.newChat),
+ )
+ }
+ >
+
+ {t.common.rename}
+
+ togglePin(thread.thread_id, !pinned)}
+ >
+ {pinned ? (
+
+ ) : (
+
+ )}
+ {pinned ? t.common.unpin : t.common.pin}
+
+
+ toggleFavorite(thread.thread_id, !favorited)
+ }
+ >
+
+
+ {favorited ? t.common.unfavorite : t.common.favorite}
+
+
+ setShareThreadId(thread.thread_id)}
+ >
+
+ {t.common.share}
+
+
+
+
+ {t.common.export}
+
+
+ handleExport(thread, "markdown")}
+ >
+
+ {t.common.exportAsMarkdown}
+
+ handleExport(thread, "json")}
+ >
+
+ {t.common.exportAsJSON}
+
+
+
+
+ handleDelete(thread.thread_id)}
+ >
+
+ {t.common.delete}
+
+
+
+ )}
+
+ );
+ })
+ )}
+
+
+ {/* 重命名弹窗 */}
+
+
+ {/* 分享弹窗 */}
+
{
+ if (!o) setShareThreadId(null);
+ }}
+ />
+
+ );
+}