ai-chatbot/components/agents/AgentPresetPanelDarkV2.tsx
Your Name f764767ba4
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: refresh agent workflow dark ui
2026-03-18 00:24:32 +08:00

468 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useMemo, useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
Loader2,
Plus,
Search,
Sparkles,
User,
Grid3X3,
Wand2,
ArrowRight,
LayoutGrid,
FolderOpen,
Building2,
Image as ImageIcon,
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 { 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;
}
export function AgentPresetPanelDarkV2({
selectedPresetId,
onSelectPreset,
onCreateClick,
embedded = false,
}: 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, 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">
<div className="relative">
<Search className="absolute left-3 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-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>
</div>
{/* 导航标签(简化版) */}
<div className="mb-4 flex 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-1 gap-3">
{isLoading ? (
<div className="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}
/>
))}
</AnimatePresence>
) : (
<div className="text-center py-8 text-white/40 text-sm">
暂无智能体
</div>
)}
</div>
<AgentPresetDialog
open={showCreateDialog || Boolean(editingPreset)}
onOpenChange={(nextOpen) => {
if (!nextOpen) {
setShowCreateDialog(false);
setEditingPreset(null);
return;
}
setShowCreateDialog(true);
}}
initialPreset={editingPreset}
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-[1800px] px-6">
<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-[1800px] px-6 py-8">
{/* 标题区域 */}
<motion.div
className="mb-8 text-center"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<h1 className="mb-3 text-3xl font-bold text-white md:text-4xl">
三倍速出片,电影级质感
</h1>
<p className="mb-6 text-base text-white/50">
第一步:风格选择 &nbsp;&nbsp; 第二步:智能体生图
</p>
<Button
className={cn(
"h-12 gap-2 rounded-full px-8",
"bg-gradient-to-r from-emerald-500 to-teal-500",
"text-white font-medium text-base",
"hover:from-emerald-400 hover:to-teal-400",
"shadow-[0_0_30px_rgba(16,185,129,0.3)]",
"transition-all duration-300"
)}
>
我要创作
<ArrowRight className="h-5 w-5" />
</Button>
</motion.div>
{/* 视频/功能卡片展示区域 - 预留空位 */}
<motion.div
className="relative mb-12 aspect-[21/9] w-full overflow-hidden rounded-2xl border border-white/10 bg-zinc-900/50"
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.6, delay: 0.2 }}
>
{/* 这里将来放视频或其他功能卡片 */}
<div className="flex h-full items-center justify-center">
<div className="text-center">
<div className="mb-4 flex h-20 w-20 items-center justify-center rounded-2xl bg-white/5 mx-auto">
<ImageIcon className="h-10 w-10 text-white/20" />
</div>
<p className="text-white/40">视频展示区(预留)</p>
<p className="mt-2 text-sm text-white/20">未来可放置宣传片或功能介绍</p>
</div>
</div>
{/* 装饰性渐变 */}
<div className="absolute inset-0 bg-gradient-to-t from-black/50 via-transparent to-transparent pointer-events-none" />
</motion.div>
</div>
</section>
{/* 智能体卡片网格区域 */}
<section className="pb-20">
<div className="mx-auto max-w-[1800px] px-6">
{/* 搜索栏 */}
<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-[1800px] px-6">
<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}
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>
);
}