239 lines
7.8 KiB
TypeScript
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);
|
|
}}
|
|
/>
|
|
</>
|
|
);
|
|
}
|