728 lines
25 KiB
TypeScript
728 lines
25 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import { motion, AnimatePresence } from "framer-motion";
|
|
import { useSession } from "next-auth/react";
|
|
import {
|
|
Loader2,
|
|
Plus,
|
|
Search,
|
|
Sparkles,
|
|
Grid3X3,
|
|
Wand2,
|
|
LayoutGrid,
|
|
FolderOpen,
|
|
Menu
|
|
} from "lucide-react";
|
|
import useSWR from "swr";
|
|
import { toast } from "sonner";
|
|
import { cn } from "@/lib/utils";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Button } from "@/components/ui/button";
|
|
import { agentService } from "@/services/agents";
|
|
import type { AgentPreset, AgentPresetLibraryCategoryNode, AgentPresetLibraryConfig } from "@/types/agent";
|
|
import { normalizeAgentWorkflowShowcaseConfig } from "@/lib/agents/workflow-showcase-config";
|
|
import {
|
|
AGENT_PRESET_LIBRARY_DEFAULT_SECTION_ID,
|
|
AGENT_PRESET_LIBRARY_MY_WORKS_ID,
|
|
getEnabledAgentPresetLibraryNodes,
|
|
getEnabledAgentPresetLibrarySections,
|
|
normalizeAgentPresetLibraryConfig,
|
|
resolveAgentPresetCategoryPath,
|
|
} from "@/lib/agents/preset-library-config";
|
|
import { AgentWorkflowShowcaseBanner } from "./AgentWorkflowShowcaseBanner";
|
|
import { AgentPresetCardDark } from "./AgentPresetCardDark";
|
|
import { AgentPresetDialog } from "./AgentPresetDialog";
|
|
|
|
interface AgentPresetPanelDarkV2Props {
|
|
selectedPresetId?: string | null;
|
|
onSelectPreset: (presetId: string | null) => void;
|
|
onCreateClick?: () => void;
|
|
embedded?: boolean;
|
|
embeddedEntityLabel?: string;
|
|
}
|
|
|
|
const AGENT_PRESET_LIBRARY_ALL_MODES_ID = "__all_modes__";
|
|
|
|
export function AgentPresetPanelDarkV2({
|
|
selectedPresetId,
|
|
onSelectPreset,
|
|
onCreateClick,
|
|
embedded = false,
|
|
embeddedEntityLabel = "智能体",
|
|
}: AgentPresetPanelDarkV2Props) {
|
|
const { data: session } = useSession();
|
|
const [activeTab, setActiveTab] = useState<string>(AGENT_PRESET_LIBRARY_ALL_MODES_ID);
|
|
const [keyword, setKeyword] = useState("");
|
|
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
|
const [editingPreset, setEditingPreset] = useState<AgentPreset | null>(null);
|
|
const [selectedLevel1Id, setSelectedLevel1Id] = useState("");
|
|
const [selectedLevel2Id, setSelectedLevel2Id] = useState("");
|
|
const [selectedLevel3Id, setSelectedLevel3Id] = useState("");
|
|
const { data: siteSettingsData } = useSWR<{
|
|
agentWorkflowShowcaseConfig?: unknown;
|
|
agentPresetLibraryConfig?: AgentPresetLibraryConfig;
|
|
}>(
|
|
"/api/site-settings",
|
|
(url: string) => fetch(url, { cache: "no-store" }).then((res) => res.json())
|
|
);
|
|
|
|
const { data, isLoading, mutate } = useSWR(
|
|
["agent-presets"],
|
|
async () => {
|
|
const result = await agentService.getPresets({
|
|
scope: "all",
|
|
sort: "latest",
|
|
});
|
|
if (!result.success || !result.data) {
|
|
throw new Error(result.error || "获取智能体失败");
|
|
}
|
|
return result.data;
|
|
}
|
|
);
|
|
const presetLibraryConfig = useMemo(
|
|
() => normalizeAgentPresetLibraryConfig(siteSettingsData?.agentPresetLibraryConfig),
|
|
[siteSettingsData?.agentPresetLibraryConfig]
|
|
);
|
|
const presetLibrarySections = useMemo(
|
|
() => getEnabledAgentPresetLibrarySections(presetLibraryConfig),
|
|
[presetLibraryConfig]
|
|
);
|
|
const navTabs = useMemo(() => {
|
|
const allModesTab = {
|
|
id: AGENT_PRESET_LIBRARY_ALL_MODES_ID,
|
|
label: "所有模式",
|
|
icon: Grid3X3,
|
|
isMyWorks: false,
|
|
isAllModes: true,
|
|
};
|
|
const dynamicTabs = presetLibrarySections.map((section) => ({
|
|
id: section.id,
|
|
label: section.label,
|
|
icon: LayoutGrid,
|
|
isMyWorks: false,
|
|
isAllModes: false,
|
|
}));
|
|
const myWorksTab = {
|
|
id: AGENT_PRESET_LIBRARY_MY_WORKS_ID,
|
|
label: "我的模式",
|
|
icon: FolderOpen,
|
|
isMyWorks: true,
|
|
isAllModes: false,
|
|
};
|
|
|
|
if (dynamicTabs.length === 0) {
|
|
return [allModesTab, myWorksTab];
|
|
}
|
|
|
|
return [allModesTab, ...dynamicTabs, myWorksTab];
|
|
}, [presetLibrarySections]);
|
|
const activeSection = useMemo(
|
|
() => presetLibrarySections.find((section) => section.id === activeTab) || null,
|
|
[activeTab, presetLibrarySections]
|
|
);
|
|
const level1Options = useMemo(
|
|
() => getEnabledAgentPresetLibraryNodes(activeSection?.categories || []),
|
|
[activeSection]
|
|
);
|
|
const selectedLevel1 = useMemo(
|
|
() => level1Options.find((node) => node.id === selectedLevel1Id) || null,
|
|
[level1Options, selectedLevel1Id]
|
|
);
|
|
const level2Options = useMemo(
|
|
() => getEnabledAgentPresetLibraryNodes(selectedLevel1?.children || []),
|
|
[selectedLevel1]
|
|
);
|
|
const selectedLevel2 = useMemo(
|
|
() => level2Options.find((node) => node.id === selectedLevel2Id) || null,
|
|
[level2Options, selectedLevel2Id]
|
|
);
|
|
const level3Options = useMemo(
|
|
() => getEnabledAgentPresetLibraryNodes(selectedLevel2?.children || []),
|
|
[selectedLevel2]
|
|
);
|
|
const isAllModesTab = activeTab === AGENT_PRESET_LIBRARY_ALL_MODES_ID;
|
|
const isMyWorksTab = activeTab === AGENT_PRESET_LIBRARY_MY_WORKS_ID;
|
|
const canManagePublicPresets =
|
|
session?.user?.type === "admin" || session?.user?.type === "agent_admin";
|
|
|
|
useEffect(() => {
|
|
if (navTabs.some((tab) => tab.id === activeTab)) return;
|
|
const nextTabId = navTabs[0]?.id || AGENT_PRESET_LIBRARY_MY_WORKS_ID;
|
|
setActiveTab(nextTabId);
|
|
}, [activeTab, navTabs]);
|
|
|
|
useEffect(() => {
|
|
setSelectedLevel1Id("");
|
|
setSelectedLevel2Id("");
|
|
setSelectedLevel3Id("");
|
|
}, [activeTab]);
|
|
|
|
useEffect(() => {
|
|
if (!selectedLevel1Id || level1Options.some((node) => node.id === selectedLevel1Id)) return;
|
|
setSelectedLevel1Id("");
|
|
setSelectedLevel2Id("");
|
|
setSelectedLevel3Id("");
|
|
}, [level1Options, selectedLevel1Id]);
|
|
|
|
useEffect(() => {
|
|
if (!selectedLevel2Id || level2Options.some((node) => node.id === selectedLevel2Id)) return;
|
|
setSelectedLevel2Id("");
|
|
setSelectedLevel3Id("");
|
|
}, [level2Options, selectedLevel2Id]);
|
|
|
|
useEffect(() => {
|
|
if (!selectedLevel3Id || level3Options.some((node) => node.id === selectedLevel3Id)) return;
|
|
setSelectedLevel3Id("");
|
|
}, [level3Options, selectedLevel3Id]);
|
|
|
|
// 根据当前标签过滤数据
|
|
const filteredPresets = useMemo(() => {
|
|
let presets: AgentPreset[] = isMyWorksTab ? data?.private || [] : data?.public || [];
|
|
|
|
if (!isMyWorksTab && !isAllModesTab) {
|
|
const fallbackSectionId =
|
|
presetLibrarySections.find((section) => section.id === AGENT_PRESET_LIBRARY_DEFAULT_SECTION_ID)?.id ||
|
|
presetLibrarySections[0]?.id ||
|
|
null;
|
|
|
|
presets = presets.filter((preset) => {
|
|
const resolvedPath = resolveAgentPresetCategoryPath({
|
|
categoryPath: preset.categoryPath || null,
|
|
config: presetLibraryConfig,
|
|
});
|
|
const presetSectionId = resolvedPath.sectionId || fallbackSectionId;
|
|
if (!presetSectionId || presetSectionId !== activeTab) {
|
|
return false;
|
|
}
|
|
if (selectedLevel1Id && resolvedPath.level1Id !== selectedLevel1Id) {
|
|
return false;
|
|
}
|
|
if (selectedLevel2Id && resolvedPath.level2Id !== selectedLevel2Id) {
|
|
return false;
|
|
}
|
|
if (selectedLevel3Id && resolvedPath.level3Id !== selectedLevel3Id) {
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
}
|
|
|
|
// 搜索过滤
|
|
if (keyword.trim()) {
|
|
const normalizedKeyword = keyword.trim().toLowerCase();
|
|
presets = presets.filter((preset) =>
|
|
[preset.name, preset.description || "", preset.type]
|
|
.filter(Boolean)
|
|
.some((value) => String(value).toLowerCase().includes(normalizedKeyword))
|
|
);
|
|
}
|
|
|
|
return presets;
|
|
}, [
|
|
activeTab,
|
|
data,
|
|
isAllModesTab,
|
|
isMyWorksTab,
|
|
keyword,
|
|
presetLibraryConfig,
|
|
presetLibrarySections,
|
|
selectedLevel1Id,
|
|
selectedLevel2Id,
|
|
selectedLevel3Id,
|
|
]);
|
|
|
|
const handleForkPreset = async (preset: AgentPreset) => {
|
|
const result = await agentService.forkPreset(preset.id, `${preset.name}(副本)`);
|
|
if (!result.success || !result.data) {
|
|
toast.error(result.error || "复制智能体失败");
|
|
return;
|
|
}
|
|
toast.success("已另存为我的智能体");
|
|
await mutate();
|
|
onSelectPreset(result.data.id);
|
|
};
|
|
|
|
const handleDeletePreset = async (preset: AgentPreset) => {
|
|
if (!window.confirm(`确定删除智能体「${preset.name}」吗?`)) {
|
|
return;
|
|
}
|
|
const result = await agentService.deletePreset(preset.id);
|
|
if (!result.success) {
|
|
toast.error(result.error || "删除智能体失败");
|
|
return;
|
|
}
|
|
toast.success("智能体已删除");
|
|
if (selectedPresetId === preset.id) {
|
|
onSelectPreset(null);
|
|
}
|
|
await mutate();
|
|
};
|
|
|
|
// 获取当前标签标题
|
|
const getCurrentTitle = () => {
|
|
if (isAllModesTab) return "所有模式";
|
|
if (isMyWorksTab) return "我的模式";
|
|
return activeSection?.label || "生图模式";
|
|
};
|
|
|
|
// 嵌入模式只渲染卡片网格
|
|
if (embedded) {
|
|
return (
|
|
<>
|
|
{/* 搜索栏 */}
|
|
<div className="mb-4 flex flex-col gap-2 sm:flex-row sm:items-center">
|
|
<div className="relative flex-1">
|
|
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-white/40" />
|
|
<Input
|
|
placeholder={`搜索${embeddedEntityLabel}...`}
|
|
value={keyword}
|
|
onChange={(e) => setKeyword(e.target.value)}
|
|
className={cn(
|
|
"h-9 rounded-lg border-0 pl-9 pr-3 text-sm",
|
|
"bg-white/5 text-white placeholder:text-white/30",
|
|
"focus:bg-white/10 focus:ring-1 focus:ring-white/20"
|
|
)}
|
|
/>
|
|
</div>
|
|
<Button
|
|
type="button"
|
|
onClick={() => (onCreateClick ? onCreateClick() : setShowCreateDialog(true))}
|
|
className={cn(
|
|
"h-9 shrink-0 gap-1.5 rounded-lg px-3 sm:w-auto",
|
|
"bg-gradient-to-r from-emerald-500 to-teal-500 text-white",
|
|
"hover:from-emerald-400 hover:to-teal-400"
|
|
)}
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
新建
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 导航标签(简化版) */}
|
|
<div className="mb-4 flex flex-wrap gap-1">
|
|
{navTabs.map((tab) => (
|
|
<button
|
|
key={tab.id}
|
|
onClick={() => setActiveTab(tab.id)}
|
|
className={cn(
|
|
"rounded-full px-3 py-1 text-xs font-medium transition-colors",
|
|
activeTab === tab.id
|
|
? "bg-white text-black"
|
|
: "text-white/60 hover:text-white hover:bg-white/5"
|
|
)}
|
|
>
|
|
{tab.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{!isMyWorksTab && !isAllModesTab ? (
|
|
<div className="mb-4 space-y-2 rounded-2xl border border-white/8 bg-white/[0.02] p-3">
|
|
<CategoryFilterRow
|
|
label="一级"
|
|
options={level1Options}
|
|
selectedId={selectedLevel1Id}
|
|
onSelect={(value) => {
|
|
setSelectedLevel1Id(value);
|
|
setSelectedLevel2Id("");
|
|
setSelectedLevel3Id("");
|
|
}}
|
|
/>
|
|
{selectedLevel1 ? (
|
|
<CategoryFilterRow
|
|
label="二级"
|
|
options={level2Options}
|
|
selectedId={selectedLevel2Id}
|
|
onSelect={(value) => {
|
|
setSelectedLevel2Id(value);
|
|
setSelectedLevel3Id("");
|
|
}}
|
|
/>
|
|
) : null}
|
|
{selectedLevel2 ? (
|
|
<CategoryFilterRow
|
|
label="三级"
|
|
options={level3Options}
|
|
selectedId={selectedLevel3Id}
|
|
onSelect={setSelectedLevel3Id}
|
|
/>
|
|
) : null}
|
|
</div>
|
|
) : null}
|
|
|
|
{/* 卡片网格 */}
|
|
<div className="grid grid-cols-1 gap-2.5 md:grid-cols-2 lg:grid-cols-3">
|
|
{isLoading ? (
|
|
<div className="col-span-full flex items-center justify-center py-8">
|
|
<Loader2 className="h-6 w-6 animate-spin text-violet-500" />
|
|
</div>
|
|
) : filteredPresets.length > 0 ? (
|
|
<AnimatePresence mode="popLayout">
|
|
{filteredPresets.map((preset, index) => (
|
|
<AgentPresetCardDark
|
|
key={preset.id}
|
|
preset={preset}
|
|
selected={selectedPresetId === preset.id}
|
|
onSelect={onSelectPreset}
|
|
onFork={preset.visibility === "public" && !canManagePublicPresets ? handleForkPreset : undefined}
|
|
onEdit={
|
|
preset.visibility === "private" || canManagePublicPresets
|
|
? setEditingPreset
|
|
: undefined
|
|
}
|
|
onDelete={
|
|
preset.visibility === "private" || canManagePublicPresets
|
|
? handleDeletePreset
|
|
: undefined
|
|
}
|
|
index={index}
|
|
compact
|
|
/>
|
|
))}
|
|
</AnimatePresence>
|
|
) : (
|
|
<div className="col-span-full rounded-2xl border border-white/10 bg-white/[0.03] px-4 py-8 text-center">
|
|
<div className="text-sm text-white/40">暂无{embeddedEntityLabel}</div>
|
|
<Button
|
|
type="button"
|
|
onClick={() => (onCreateClick ? onCreateClick() : setShowCreateDialog(true))}
|
|
className={cn(
|
|
"mt-4 h-9 gap-1.5 rounded-lg px-4",
|
|
"bg-gradient-to-r from-emerald-500 to-teal-500 text-white",
|
|
"hover:from-emerald-400 hover:to-teal-400"
|
|
)}
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
新建{embeddedEntityLabel}
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<AgentPresetDialog
|
|
open={showCreateDialog || Boolean(editingPreset)}
|
|
onOpenChange={(nextOpen) => {
|
|
if (!nextOpen) {
|
|
setShowCreateDialog(false);
|
|
setEditingPreset(null);
|
|
return;
|
|
}
|
|
setShowCreateDialog(true);
|
|
}}
|
|
initialPreset={editingPreset}
|
|
entityLabel={embeddedEntityLabel}
|
|
theme="dark"
|
|
onSaved={async (preset) => {
|
|
await mutate();
|
|
onSelectPreset(preset.id);
|
|
setEditingPreset(null);
|
|
setShowCreateDialog(false);
|
|
}}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-black">
|
|
{/* 顶部固定导航栏 */}
|
|
<header className="fixed top-0 left-0 right-0 z-50 border-b border-white/5 bg-black/80 backdrop-blur-xl">
|
|
<div className="mx-auto max-w-[1480px] px-6 lg:px-8 xl:px-10">
|
|
<div className="flex h-16 items-center justify-between">
|
|
{/* Logo & Site Name */}
|
|
<div className="flex items-center gap-3">
|
|
<button className="flex h-9 w-9 items-center justify-center rounded-lg bg-white/5 text-white/60 hover:bg-white/10 hover:text-white transition-colors">
|
|
<Menu className="h-5 w-5" />
|
|
</button>
|
|
<div className="flex items-center gap-2.5">
|
|
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-violet-500 to-purple-600">
|
|
<Wand2 className="h-4 w-4 text-white" />
|
|
</div>
|
|
<span className="text-base font-semibold text-white">AI创作平台</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 导航标签 */}
|
|
<nav className="flex items-center gap-1 rounded-full bg-white/5 p-1">
|
|
{navTabs.map((tab) => {
|
|
const Icon = tab.icon;
|
|
const isActive = activeTab === tab.id;
|
|
return (
|
|
<button
|
|
key={tab.id}
|
|
onClick={() => setActiveTab(tab.id)}
|
|
className={cn(
|
|
"flex items-center gap-2 rounded-full px-4 py-2 text-sm font-medium transition-all duration-300",
|
|
isActive
|
|
? "bg-white text-black"
|
|
: "text-white/60 hover:text-white hover:bg-white/5"
|
|
)}
|
|
>
|
|
<Icon className="h-4 w-4" />
|
|
{tab.label}
|
|
</button>
|
|
);
|
|
})}
|
|
</nav>
|
|
|
|
{/* 右侧操作 */}
|
|
<div className="flex items-center gap-3">
|
|
<Button
|
|
onClick={() => onCreateClick ? onCreateClick() : setShowCreateDialog(true)}
|
|
className={cn(
|
|
"h-9 gap-2 rounded-full px-5",
|
|
"bg-white text-black hover:bg-white/90",
|
|
"font-medium text-sm transition-all",
|
|
"hover:shadow-[0_0_20px_rgba(255,255,255,0.2)]"
|
|
)}
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
新建
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Hero Section - 视频/功能展示区 */}
|
|
<section className="relative pt-16">
|
|
<div className="mx-auto max-w-[1480px] px-6 py-8 lg:px-8 xl:px-10">
|
|
<AgentWorkflowShowcaseBanner
|
|
config={normalizeAgentWorkflowShowcaseConfig(siteSettingsData?.agentWorkflowShowcaseConfig)}
|
|
onActionClick={() => {
|
|
const target = document.getElementById("agent-preset-grid");
|
|
if (!target) return;
|
|
target.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
}}
|
|
/>
|
|
</div>
|
|
</section>
|
|
|
|
{/* 智能体卡片网格区域 */}
|
|
<section className="pb-20">
|
|
<div id="agent-preset-grid" className="mx-auto max-w-[1480px] px-6 lg:px-8 xl:px-10">
|
|
{/* 搜索栏 */}
|
|
<motion.div
|
|
className="mb-8 flex items-center gap-4"
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.5, delay: 0.3 }}
|
|
>
|
|
<div className="relative flex-1 max-w-md">
|
|
<Search className="absolute left-4 top-1/2 h-4 w-4 -translate-y-1/2 text-white/40" />
|
|
<Input
|
|
placeholder="搜索生图模式..."
|
|
value={keyword}
|
|
onChange={(e) => setKeyword(e.target.value)}
|
|
className={cn(
|
|
"h-11 rounded-full border-0 pl-11 pr-4",
|
|
"bg-white/5 text-white placeholder:text-white/30",
|
|
"focus:bg-white/10 focus:ring-1 focus:ring-white/20",
|
|
"transition-all duration-300"
|
|
)}
|
|
/>
|
|
</div>
|
|
<div className="flex items-center gap-2 text-sm text-white/50">
|
|
<Grid3X3 className="h-4 w-4" />
|
|
<span>共 {filteredPresets.length} 个</span>
|
|
</div>
|
|
</motion.div>
|
|
|
|
{!isMyWorksTab && !isAllModesTab ? (
|
|
<motion.div
|
|
className="mb-6 space-y-2 rounded-2xl border border-white/8 bg-white/[0.02] p-4"
|
|
initial={{ opacity: 0, y: 16 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.4, delay: 0.35 }}
|
|
>
|
|
<CategoryFilterRow
|
|
label="一级分类"
|
|
options={level1Options}
|
|
selectedId={selectedLevel1Id}
|
|
onSelect={(value) => {
|
|
setSelectedLevel1Id(value);
|
|
setSelectedLevel2Id("");
|
|
setSelectedLevel3Id("");
|
|
}}
|
|
/>
|
|
{selectedLevel1 ? (
|
|
<CategoryFilterRow
|
|
label="二级分类"
|
|
options={level2Options}
|
|
selectedId={selectedLevel2Id}
|
|
onSelect={(value) => {
|
|
setSelectedLevel2Id(value);
|
|
setSelectedLevel3Id("");
|
|
}}
|
|
/>
|
|
) : null}
|
|
{selectedLevel2 ? (
|
|
<CategoryFilterRow
|
|
label="三级分类"
|
|
options={level3Options}
|
|
selectedId={selectedLevel3Id}
|
|
onSelect={setSelectedLevel3Id}
|
|
/>
|
|
) : null}
|
|
</motion.div>
|
|
) : null}
|
|
|
|
{/* 区域标题 */}
|
|
<motion.div
|
|
className="mb-6 flex items-center gap-3"
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
transition={{ delay: 0.4 }}
|
|
>
|
|
<h2 className="text-lg font-semibold text-white">{getCurrentTitle()}</h2>
|
|
<span className="rounded-full bg-white/5 px-3 py-1 text-xs text-white/50">
|
|
{filteredPresets.length}
|
|
</span>
|
|
</motion.div>
|
|
|
|
{/* 卡片网格 */}
|
|
<div className="grid grid-cols-2 gap-5 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6">
|
|
<AnimatePresence mode="popLayout">
|
|
{isLoading ? (
|
|
<div className="col-span-full flex items-center justify-center py-20">
|
|
<Loader2 className="h-8 w-8 animate-spin text-violet-500" />
|
|
</div>
|
|
) : filteredPresets.length > 0 ? (
|
|
filteredPresets.map((preset, index) => (
|
|
<AgentPresetCardDark
|
|
key={preset.id}
|
|
preset={preset}
|
|
selected={selectedPresetId === preset.id}
|
|
onSelect={onSelectPreset}
|
|
onFork={preset.visibility === "public" && !canManagePublicPresets ? handleForkPreset : undefined}
|
|
onEdit={
|
|
preset.visibility === "private" || canManagePublicPresets
|
|
? setEditingPreset
|
|
: undefined
|
|
}
|
|
onDelete={
|
|
preset.visibility === "private" || canManagePublicPresets
|
|
? handleDeletePreset
|
|
: undefined
|
|
}
|
|
index={index}
|
|
/>
|
|
))
|
|
) : (
|
|
<EmptyState message={`暂无${getCurrentTitle()}`} />
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* 页脚 */}
|
|
<footer className="border-t border-white/5 py-6">
|
|
<div className="mx-auto max-w-[1480px] px-6 lg:px-8 xl:px-10">
|
|
<div className="flex items-center justify-center gap-6 text-sm text-white/30">
|
|
<a href="#" className="hover:text-white/60 transition-colors">使用协议</a>
|
|
<a href="#" className="hover:text-white/60 transition-colors">隐私条款</a>
|
|
<span>© 2024 AI创作平台</span>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
|
|
<AgentPresetDialog
|
|
open={showCreateDialog || Boolean(editingPreset)}
|
|
onOpenChange={(nextOpen) => {
|
|
if (!nextOpen) {
|
|
setShowCreateDialog(false);
|
|
setEditingPreset(null);
|
|
return;
|
|
}
|
|
setShowCreateDialog(true);
|
|
}}
|
|
initialPreset={editingPreset}
|
|
entityLabel="生图模式"
|
|
theme="dark"
|
|
onSaved={async (preset) => {
|
|
await mutate();
|
|
onSelectPreset(preset.id);
|
|
setEditingPreset(null);
|
|
setShowCreateDialog(false);
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function EmptyState({ message }: { message: string }) {
|
|
return (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
className="col-span-full flex flex-col items-center justify-center rounded-2xl border border-white/5 bg-white/[0.02] py-20"
|
|
>
|
|
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-white/5">
|
|
<Sparkles className="h-7 w-7 text-white/30" />
|
|
</div>
|
|
<p className="text-sm text-white/40">{message}</p>
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
function CategoryFilterRow({
|
|
label,
|
|
options,
|
|
selectedId,
|
|
onSelect,
|
|
dark = true,
|
|
}: {
|
|
label: string;
|
|
options: AgentPresetLibraryCategoryNode[];
|
|
selectedId?: string;
|
|
onSelect: (value: string) => void;
|
|
dark?: boolean;
|
|
}) {
|
|
if (!options.length) return null;
|
|
|
|
return (
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<span className={cn("text-xs", dark ? "text-white/38" : "text-muted-foreground")}>
|
|
{label}
|
|
</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => onSelect("")}
|
|
className={cn(
|
|
"rounded-full border px-3 py-1 text-xs transition-colors",
|
|
!selectedId
|
|
? dark
|
|
? "border-white/30 bg-white text-black"
|
|
: "border-primary bg-primary/10 text-primary"
|
|
: dark
|
|
? "border-white/10 bg-white/[0.03] text-white/62 hover:border-white/20 hover:text-white"
|
|
: "border-border bg-background text-muted-foreground hover:text-foreground"
|
|
)}
|
|
>
|
|
全部
|
|
</button>
|
|
{options.map((option) => (
|
|
<button
|
|
key={option.id}
|
|
type="button"
|
|
onClick={() => onSelect(option.id)}
|
|
className={cn(
|
|
"rounded-full border px-3 py-1 text-xs transition-colors",
|
|
selectedId === option.id
|
|
? dark
|
|
? "border-emerald-300/35 bg-emerald-300/90 text-black"
|
|
: "border-primary bg-primary/10 text-primary"
|
|
: dark
|
|
? "border-white/10 bg-white/[0.03] text-white/62 hover:border-white/20 hover:text-white"
|
|
: "border-border bg-background text-muted-foreground hover:text-foreground"
|
|
)}
|
|
>
|
|
{option.name}
|
|
</button>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|