462 lines
16 KiB
TypeScript
462 lines
16 KiB
TypeScript
"use client";
|
|
|
|
import { useMemo, useState } from "react";
|
|
import { motion, AnimatePresence } from "framer-motion";
|
|
import {
|
|
Loader2,
|
|
Plus,
|
|
Search,
|
|
Sparkles,
|
|
User,
|
|
Grid3X3,
|
|
Wand2,
|
|
LayoutGrid,
|
|
FolderOpen,
|
|
Building2,
|
|
Menu
|
|
} from "lucide-react";
|
|
import useSWR from "swr";
|
|
import { toast } from "sonner";
|
|
import { cn } from "@/lib/utils";
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Button } from "@/components/ui/button";
|
|
import { agentService } from "@/services/agents";
|
|
import type { AgentPreset } from "@/types/agent";
|
|
import { normalizeAgentWorkflowShowcaseConfig } from "@/lib/agents/workflow-showcase-config";
|
|
import { AgentWorkflowShowcaseBanner } from "./AgentWorkflowShowcaseBanner";
|
|
import { AgentPresetCardDark } from "./AgentPresetCardDark";
|
|
import { AgentPresetDialog } from "./AgentPresetDialog";
|
|
|
|
// 导航标签类型
|
|
type NavTab = "cases" | "my-works" | "personal-assets" | "company-assets";
|
|
|
|
interface AgentPresetPanelDarkV2Props {
|
|
selectedPresetId?: string | null;
|
|
onSelectPreset: (presetId: string | null) => void;
|
|
onCreateClick?: () => void;
|
|
embedded?: boolean;
|
|
embeddedEntityLabel?: string;
|
|
}
|
|
|
|
export function AgentPresetPanelDarkV2({
|
|
selectedPresetId,
|
|
onSelectPreset,
|
|
onCreateClick,
|
|
embedded = false,
|
|
embeddedEntityLabel = "智能体",
|
|
}: AgentPresetPanelDarkV2Props) {
|
|
const [activeTab, setActiveTab] = useState<NavTab>("cases");
|
|
const [keyword, setKeyword] = useState("");
|
|
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
|
const [editingPreset, setEditingPreset] = useState<AgentPreset | null>(null);
|
|
const { data: siteSettingsData } = useSWR<{ agentWorkflowShowcaseConfig?: unknown }>(
|
|
!embedded ? "/api/site-settings" : null,
|
|
(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 filteredPresets = useMemo(() => {
|
|
let presets: AgentPreset[] = [];
|
|
|
|
switch (activeTab) {
|
|
case "cases":
|
|
presets = data?.public || [];
|
|
break;
|
|
case "my-works":
|
|
presets = data?.private || [];
|
|
break;
|
|
case "personal-assets":
|
|
// 这里可以接入个人资产库数据
|
|
presets = data?.private || [];
|
|
break;
|
|
case "company-assets":
|
|
// 这里可以接入公司资产库数据
|
|
presets = data?.public || [];
|
|
break;
|
|
}
|
|
|
|
// 搜索过滤
|
|
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;
|
|
}, [data, activeTab, keyword]);
|
|
|
|
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 navTabs = [
|
|
{ id: "cases" as NavTab, label: "案例广场", icon: LayoutGrid },
|
|
{ id: "my-works" as NavTab, label: "我的作品", icon: FolderOpen },
|
|
{ id: "personal-assets" as NavTab, label: "个人资产库", icon: User },
|
|
{ id: "company-assets" as NavTab, label: "公司资产库", icon: Building2 },
|
|
];
|
|
|
|
// 获取当前标签标题
|
|
const getCurrentTitle = () => {
|
|
switch (activeTab) {
|
|
case "cases": return "公共智能体";
|
|
case "my-works": return "我的智能体";
|
|
case "personal-assets": return "个人资产库";
|
|
case "company-assets": return "公司资产库";
|
|
}
|
|
};
|
|
|
|
// 嵌入模式只渲染卡片网格
|
|
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.slice(0, 2).map((tab) => (
|
|
<button
|
|
key={tab.id}
|
|
onClick={() => setActiveTab(tab.id as NavTab)}
|
|
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>
|
|
|
|
{/* 卡片网格 */}
|
|
<div className="grid grid-cols-[repeat(auto-fill,minmax(96px,1fr))] gap-2.5">
|
|
{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={handleForkPreset}
|
|
onEdit={setEditingPreset}
|
|
onDelete={handleDeletePreset}
|
|
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>
|
|
|
|
{/* 区域标题 */}
|
|
<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={handleForkPreset}
|
|
onEdit={setEditingPreset}
|
|
onDelete={handleDeletePreset}
|
|
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>
|
|
);
|
|
}
|