ai-chatbot/components/agents/AgentPresetPanelNew.tsx
Your Name 8757f76b1f
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
chore: rename agent UI to stylized
2026-04-11 19:48:45 +08:00

239 lines
7.8 KiB
TypeScript

"use client";
import { useMemo, useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Loader2, Sparkles, User } from "lucide-react";
import useSWR from "swr";
import { toast } from "sonner";
import { cn } from "@/lib/utils";
import { ScrollArea } from "@/components/ui/scroll-area";
import { agentService } from "@/services/agents";
import type { AgentPreset } from "@/types/agent";
import { AgentPresetCardNew } from "./AgentPresetCardNew";
import { AgentPresetDialog } from "./AgentPresetDialog";
import { AgentPanelHeader } from "./AgentPanelHeader";
import { AgentSectionHeader } from "./AgentSectionHeader";
interface AgentPresetPanelNewProps {
selectedPresetId?: string | null;
onSelectPreset: (presetId: string | null) => void;
embedded?: boolean;
layout?: "fill" | "inline";
title?: string;
description?: string;
searchPlaceholder?: string;
publicTitle?: string;
privateTitle?: string;
}
export function AgentPresetPanelNew({
selectedPresetId,
onSelectPreset,
embedded = false,
layout = "fill",
title = "风格化",
description = "选择模式后,工作台会自动加载对应槽位和生成能力",
searchPlaceholder = "搜索风格化...",
publicTitle = "公共风格化",
privateTitle = "我的风格化",
}: AgentPresetPanelNewProps) {
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 filteredPublicPresets = useMemo(() => {
return (data?.public || []).filter((preset) => {
const normalizedKeyword = keyword.trim().toLowerCase();
if (!normalizedKeyword) return true;
return [preset.name, preset.description || "", preset.type]
.filter(Boolean)
.some((value) => String(value).toLowerCase().includes(normalizedKeyword));
});
}, [data?.public, keyword]);
const filteredPrivatePresets = useMemo(() => {
return (data?.private || []).filter((preset) => {
const normalizedKeyword = keyword.trim().toLowerCase();
if (!normalizedKeyword) return true;
return [preset.name, preset.description || "", preset.type]
.filter(Boolean)
.some((value) => String(value).toLowerCase().includes(normalizedKeyword));
});
}, [data?.private, 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 renderEmptyState = (message: string) => (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="rounded-2xl border border-dashed border-slate-200 bg-slate-50/50 p-8 text-center dark:border-slate-800 dark:bg-slate-900/30"
>
<div className="mx-auto mb-3 flex h-12 w-12 items-center justify-center rounded-2xl bg-slate-100 dark:bg-slate-800">
<Sparkles className="h-5 w-5 text-slate-400" />
</div>
<p className="text-sm text-slate-500 dark:text-slate-400">{message}</p>
</motion.div>
);
const renderLoadingState = () => (
<div className="flex items-center justify-center py-12">
<div className="flex flex-col items-center gap-3">
<Loader2 className="h-6 w-6 animate-spin text-indigo-500" />
<span className="text-xs text-slate-400">加载中...</span>
</div>
</div>
);
const content = (
<div className="space-y-6 p-5">
{/* 公共风格化区域 */}
<section className="space-y-3">
<AgentSectionHeader
title={publicTitle}
count={filteredPublicPresets.length}
icon={<Sparkles className="h-3 w-3" />}
delay={0.1}
/>
{isLoading ? (
renderLoadingState()
) : filteredPublicPresets.length > 0 ? (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<AnimatePresence mode="popLayout">
{filteredPublicPresets.map((preset, index) => (
<AgentPresetCardNew
key={preset.id}
preset={preset}
selected={selectedPresetId === preset.id}
onSelect={onSelectPreset}
onFork={handleForkPreset}
onEdit={setEditingPreset}
onDelete={handleDeletePreset}
index={index}
/>
))}
</AnimatePresence>
</div>
) : (
renderEmptyState("暂无公共风格化")
)}
</section>
{/* 私有风格化区域 */}
<section className="space-y-3">
<AgentSectionHeader
title={privateTitle}
count={filteredPrivatePresets.length}
icon={<User className="h-3 w-3" />}
delay={0.2}
/>
{isLoading ? null : filteredPrivatePresets.length > 0 ? (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<AnimatePresence mode="popLayout">
{filteredPrivatePresets.map((preset, index) => (
<AgentPresetCardNew
key={preset.id}
preset={preset}
selected={selectedPresetId === preset.id}
onSelect={onSelectPreset}
onEdit={setEditingPreset}
onDelete={handleDeletePreset}
index={index + filteredPublicPresets.length}
/>
))}
</AnimatePresence>
</div>
) : (
renderEmptyState("还没有私有风格化,点击右上角新建一个吧")
)}
</section>
</div>
);
return (
<>
<div
className={cn(
"flex flex-col bg-white dark:bg-slate-950",
layout === "fill" ? "h-full" : "h-auto",
embedded ? "" : "border-r border-slate-200/60 dark:border-slate-800/60"
)}
>
<AgentPanelHeader
title={title}
description={description}
searchValue={keyword}
onSearchChange={setKeyword}
searchPlaceholder={searchPlaceholder}
onCreateClick={() => setShowCreateDialog(true)}
createLabel="新建"
/>
{layout === "fill" ? (
<ScrollArea className="flex-1">{content}</ScrollArea>
) : (
content
)}
</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);
}}
/>
</>
);
}