- /login/admin 登录时自动赋予 admin 角色,允许编辑内置专家 - agent列表页新增「全部」(内置+已发布) 和「自己的」两个Tab Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
118 lines
3.8 KiB
TypeScript
118 lines
3.8 KiB
TypeScript
"use client";
|
|
|
|
import { BotIcon, PlusIcon } from "lucide-react";
|
|
import { useRouter } from "next/navigation";
|
|
import { useMemo, useState } from "react";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { useAgents } from "@/core/agents";
|
|
import { getStoredAuth } from "@/core/auth";
|
|
import { useI18n } from "@/core/i18n/hooks";
|
|
|
|
import { AgentCard } from "./agent-card";
|
|
|
|
export function AgentGallery() {
|
|
const { t } = useI18n();
|
|
const { agents, isLoading } = useAgents();
|
|
const router = useRouter();
|
|
const [activeTab, setActiveTab] = useState<"all" | "mine">("all");
|
|
|
|
const storedAuth = useMemo(() => getStoredAuth(), []);
|
|
const currentUserId = storedAuth?.user_id ?? null;
|
|
|
|
const allAgents = useMemo(
|
|
() => agents.filter((a) => a.builtin || a.published),
|
|
[agents],
|
|
);
|
|
|
|
const myAgents = useMemo(
|
|
() => agents.filter((a) => a.user_id !== null && a.user_id === currentUserId),
|
|
[agents, currentUserId],
|
|
);
|
|
|
|
const handleNewAgent = () => {
|
|
router.push("/page/workspace/agents/new");
|
|
};
|
|
|
|
const renderGrid = (list: typeof agents) => (
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
|
{list.map((agent) => (
|
|
<AgentCard key={agent.id} agent={agent} />
|
|
))}
|
|
</div>
|
|
);
|
|
|
|
const renderEmpty = (description: string, showNewButton = false) => (
|
|
<div className="flex h-64 flex-col items-center justify-center gap-3 text-center">
|
|
<div className="bg-muted flex h-14 w-14 items-center justify-center rounded-full">
|
|
<BotIcon className="text-muted-foreground h-7 w-7" />
|
|
</div>
|
|
<div>
|
|
<p className="font-medium">{t.agents.emptyTitle}</p>
|
|
<p className="text-muted-foreground mt-1 text-sm">{description}</p>
|
|
</div>
|
|
{showNewButton && (
|
|
<Button variant="outline" className="mt-2" onClick={handleNewAgent}>
|
|
<PlusIcon className="mr-1.5 h-4 w-4" />
|
|
{t.agents.newAgent}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className="flex size-full flex-col">
|
|
{/* Page header */}
|
|
<div className="flex items-center justify-between border-b px-6 py-4">
|
|
<div>
|
|
<h1 className="text-xl font-semibold">{t.agents.title}</h1>
|
|
<p className="text-muted-foreground mt-0.5 text-sm">
|
|
{t.agents.description}
|
|
</p>
|
|
</div>
|
|
<Button onClick={handleNewAgent}>
|
|
<PlusIcon className="mr-1.5 h-4 w-4" />
|
|
{t.agents.newAgent}
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Tabs */}
|
|
<Tabs
|
|
value={activeTab}
|
|
onValueChange={(v) => setActiveTab(v as "all" | "mine")}
|
|
className="flex flex-1 flex-col overflow-hidden"
|
|
>
|
|
<TabsList className="mx-6 mt-4 w-auto self-start">
|
|
<TabsTrigger value="all">全部</TabsTrigger>
|
|
<TabsTrigger value="mine">自己的</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="all" className="flex-1 overflow-y-auto p-6 mt-0">
|
|
{isLoading ? (
|
|
<div className="text-muted-foreground flex h-40 items-center justify-center text-sm">
|
|
{t.common.loading}
|
|
</div>
|
|
) : allAgents.length === 0 ? (
|
|
renderEmpty(t.agents.emptyDescription)
|
|
) : (
|
|
renderGrid(allAgents)
|
|
)}
|
|
</TabsContent>
|
|
|
|
<TabsContent value="mine" className="flex-1 overflow-y-auto p-6 mt-0">
|
|
{isLoading ? (
|
|
<div className="text-muted-foreground flex h-40 items-center justify-center text-sm">
|
|
{t.common.loading}
|
|
</div>
|
|
) : myAgents.length === 0 ? (
|
|
renderEmpty(t.agents.emptyDescription, true)
|
|
) : (
|
|
renderGrid(myAgents)
|
|
)}
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
);
|
|
}
|