"use client"; import { ArrowDownUpIcon, BotIcon, LockIcon, PlusIcon, ShieldIcon } from "lucide-react"; import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Pagination } from "@/components/ui/pagination"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { TagSearchBar } from "@/components/workspace/tags/tag-search-bar"; import { ViewModeToggle } from "@/components/workspace/view-mode-toggle"; import { useAgentsPaginated, usePrivateAgentsPaginated } from "@/core/agents"; import type { Agent, AgentSort } from "@/core/agents"; import { useI18n } from "@/core/i18n/hooks"; import { usePrivateSquareStatus, useViewMode } from "@/core/system-settings/hooks"; import { AgentCard } from "./agent-card"; const PAGE_SIZE = 12; // Within a single tab the verified password is kept in sessionStorage so a // refresh / route navigation does not re-prompt. Cleared on tab close, so the // password never persists beyond the browsing session. const PRIVATE_SQUARE_PASSWORD_KEY = "deer-flow.private-square-password"; export function AgentGallery() { const { t } = useI18n(); const router = useRouter(); const [viewMode, setViewMode] = useViewMode("agent"); const [activeTab, setActiveTab] = useState<"all" | "mine" | "private">( "mine", ); const [search, setSearch] = useState(""); const [tagIds, setTagIds] = useState([]); const [sort, setSort] = useState("time_desc"); // Per-tab 1-based page index, preserved while switching tabs. const [pages, setPages] = useState<{ all: number; mine: number; private: number }>( { all: 1, mine: 1, private: 1 }, ); // Search + tag filtering are both server-side now. The backend mirrors the // frontend's desensitize map, so typing the displayed abbreviation (e.g. // "TS") still matches the stored raw name (e.g. "态势分析"). Whenever the // filters change, every tab returns to its first page. const tagKey = tagIds.join(","); useEffect(() => { setPages({ all: 1, mine: 1, private: 1 }); }, [search, tagKey, sort]); const { data: privateStatus } = usePrivateSquareStatus(); const privateFeatureAvailable = !!privateStatus?.enabled; const privateFeatureUsable = privateFeatureAvailable && !!privateStatus?.password_set; const [privatePassword, setPrivatePassword] = useState(() => { if (typeof window === "undefined") return null; return window.sessionStorage.getItem(PRIVATE_SQUARE_PASSWORD_KEY); }); const [passwordInput, setPasswordInput] = useState(""); // Only the active tab fetches. "广场" maps to the server "square" scope // (built-in/published, never private), "我的" to "mine" (owned + favorited). const mainScope = activeTab === "mine" ? "mine" : "square"; const mainPage = activeTab === "mine" ? pages.mine : pages.all; const { agents: mainAgents, total: mainTotal, isLoading: mainLoading, } = useAgentsPaginated({ scope: mainScope, search, tagIds, sort, page: mainPage, pageSize: PAGE_SIZE, enabled: activeTab !== "private", }); const { agents: privateAgents, total: privateTotal, isLoading: isPrivateLoading, error: privateError, } = usePrivateAgentsPaginated(privatePassword, { search, tagIds, sort, page: pages.private, pageSize: PAGE_SIZE, enabled: activeTab === "private", }); // If the server reports the stored password no longer works (likely the // admin rotated it), drop it from session storage and re-prompt. useEffect(() => { if (privatePassword && privateError) { window.sessionStorage.removeItem(PRIVATE_SQUARE_PASSWORD_KEY); setPrivatePassword(null); toast.error("私密广场密码已失效,请重新输入"); } }, [privateError, privatePassword]); const activeTotal = activeTab === "private" ? privateTotal : mainTotal; const setTabPage = (tab: "all" | "mine" | "private", page: number) => setPages((prev) => ({ ...prev, [tab]: page })); const handleNewAgent = () => { router.push("/page/workspace/agents/new"); }; const handleSubmitPassword = () => { const trimmed = passwordInput.trim(); if (!trimmed) { toast.error("请输入密码"); return; } window.sessionStorage.setItem(PRIVATE_SQUARE_PASSWORD_KEY, trimmed); setPrivatePassword(trimmed); setPasswordInput(""); }; const handleLockPrivate = () => { window.sessionStorage.removeItem(PRIVATE_SQUARE_PASSWORD_KEY); setPrivatePassword(null); }; const renderGrid = (list: Agent[]) => viewMode === "list" ? (
{list.map((agent) => ( ))}
) : (
{list.map((agent) => ( ))}
); /** Loading / empty / (grid + pagination) for one paginated tab. */ const renderListBody = (opts: { list: Agent[]; total: number; isLoading: boolean; tab: "all" | "mine" | "private"; emptyDescription: string; showNewButton?: boolean; }) => { const { list, total, isLoading, tab, emptyDescription, showNewButton } = opts; // keepPreviousData keeps the old page visible while a new page loads, so // only show the spinner on the very first load (no rows yet). if (isLoading && list.length === 0) { return (
{t.common.loading}
); } if (list.length === 0) { return renderEmpty(emptyDescription, showNewButton); } return (
{renderGrid(list)} setTabPage(tab, p)} className="pt-2" />
); }; const renderEmpty = (description: string, showNewButton = false) => (

{t.agents.emptyTitle}

{description}

{showNewButton && ( )}
); const renderPrivateTab = () => { if (!privateFeatureAvailable) { return (

私密广场尚未开启

请管理员前往「综合管理 → 私密广场配置」开启该功能并设置密码。

); } if (!privateFeatureUsable) { return (

私密广场未配置密码

请管理员在私密广场配置页设置一个访问密码。

); } if (!privatePassword) { return (

私密广场

请输入访问密码,验证通过后才能查看其中的智能体。

setPasswordInput(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") handleSubmitPassword(); }} // This is a one-off access code, not a login credential. Tell // browsers / password managers not to save or autofill it (which // would also stop them from spilling a saved account into the // name search box on this page). name="private-square-access-code" autoComplete="off" data-1p-ignore data-lpignore="true" data-form-type="other" />
); } return renderListBody({ list: privateAgents, total: privateTotal, isLoading: isPrivateLoading, tab: "private", emptyDescription: "私密广场暂时还没有智能体。", }); }; return (
{/* Page header */}

{t.agents.title}

{t.agents.description}

{/* Tabs */} setActiveTab(v as "all" | "mine" | "private") } className="flex flex-1 flex-col overflow-hidden" >
我的 广场 {privateFeatureAvailable && ( 私密广场 )}
共 {activeTotal} 个 {activeTab === "private" && privatePassword && ( )}
setSort(v as AgentSort)}> 最新创建 最早创建 最近更新 最早更新 名称 A-Z 名称 Z-A } />
{renderListBody({ list: mainAgents, total: mainTotal, isLoading: mainLoading, tab: "all", emptyDescription: t.agents.emptyDescription, })} {renderListBody({ list: mainAgents, total: mainTotal, isLoading: mainLoading, tab: "mine", emptyDescription: t.agents.emptyDescription, showNewButton: true, })} {privateFeatureAvailable && ( {renderPrivateTab()} )}
); }