270 lines
9.6 KiB
TypeScript
270 lines
9.6 KiB
TypeScript
"use client";
|
||
|
||
import { useMemo, useState } from "react";
|
||
import { Loader2, Plus, Sparkles } from "lucide-react";
|
||
import useSWR from "swr";
|
||
import { toast } from "sonner";
|
||
import { cn } from "@/lib/utils";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Input } from "@/components/ui/input";
|
||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||
import { agentService } from "@/services/agents";
|
||
import type { AgentPreset } from "@/types/agent";
|
||
import { AgentPresetCard } from "@/components/agents/AgentPresetCard";
|
||
import { AgentPresetDialog } from "@/components/agents/AgentPresetDialog";
|
||
|
||
interface AgentPresetPanelProps {
|
||
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 AgentPresetPanel({
|
||
selectedPresetId,
|
||
onSelectPreset,
|
||
embedded = false,
|
||
layout = "fill",
|
||
title = "风格化",
|
||
description = "选择模式后,工作台会自动加载对应槽位和生成能力",
|
||
searchPlaceholder = "搜索风格化",
|
||
publicTitle = "公共风格化",
|
||
privateTitle = "我的风格化",
|
||
}: AgentPresetPanelProps) {
|
||
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();
|
||
};
|
||
|
||
return (
|
||
<>
|
||
<div
|
||
className={cn(
|
||
"flex flex-col bg-white dark:bg-zinc-950",
|
||
layout === "fill" ? "h-full" : "h-auto",
|
||
embedded ? "" : "border-r"
|
||
)}
|
||
>
|
||
<div className="border-b p-4">
|
||
<div className="mb-3 flex items-center justify-between">
|
||
<div>
|
||
<div className="flex items-center gap-2 text-sm font-semibold">
|
||
<Sparkles className="h-4 w-4 text-amber-500" />
|
||
{title}
|
||
</div>
|
||
<div className="mt-1 text-xs text-muted-foreground">
|
||
{description}
|
||
</div>
|
||
</div>
|
||
<Button size="sm" onClick={() => setShowCreateDialog(true)}>
|
||
<Plus className="mr-1 h-4 w-4" />
|
||
新建
|
||
</Button>
|
||
</div>
|
||
<Input
|
||
placeholder={searchPlaceholder}
|
||
value={keyword}
|
||
onChange={(event) => setKeyword(event.target.value)}
|
||
/>
|
||
</div>
|
||
|
||
{layout === "fill" ? (
|
||
<ScrollArea className="flex-1">
|
||
<div className="space-y-5 p-4">
|
||
<section className="space-y-3">
|
||
<div className="text-sm font-medium">{publicTitle}</div>
|
||
{isLoading ? (
|
||
<div className="flex items-center justify-center py-8">
|
||
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
||
</div>
|
||
) : filteredPublicPresets.length > 0 ? (
|
||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
||
{filteredPublicPresets.map((preset) => (
|
||
<AgentPresetCard
|
||
key={preset.id}
|
||
preset={preset}
|
||
selected={selectedPresetId === preset.id}
|
||
onSelect={onSelectPreset}
|
||
onFork={handleForkPreset}
|
||
onEdit={setEditingPreset}
|
||
onDelete={handleDeletePreset}
|
||
/>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div className="rounded-xl border border-dashed p-4 text-sm text-muted-foreground">
|
||
暂无公共风格化
|
||
</div>
|
||
)}
|
||
</section>
|
||
|
||
<section className="space-y-3">
|
||
<div className="text-sm font-medium">{privateTitle}</div>
|
||
{isLoading ? null : filteredPrivatePresets.length > 0 ? (
|
||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
||
{filteredPrivatePresets.map((preset) => (
|
||
<AgentPresetCard
|
||
key={preset.id}
|
||
preset={preset}
|
||
selected={selectedPresetId === preset.id}
|
||
onSelect={onSelectPreset}
|
||
onEdit={setEditingPreset}
|
||
onDelete={handleDeletePreset}
|
||
/>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div className="rounded-xl border border-dashed p-4 text-sm text-muted-foreground">
|
||
还没有私有风格化,先新建一个吧
|
||
</div>
|
||
)}
|
||
</section>
|
||
</div>
|
||
</ScrollArea>
|
||
) : (
|
||
<div className="space-y-5 p-4">
|
||
<section className="space-y-3">
|
||
<div className="text-sm font-medium">{publicTitle}</div>
|
||
{isLoading ? (
|
||
<div className="flex items-center justify-center py-8">
|
||
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
||
</div>
|
||
) : filteredPublicPresets.length > 0 ? (
|
||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
||
{filteredPublicPresets.map((preset) => (
|
||
<AgentPresetCard
|
||
key={preset.id}
|
||
preset={preset}
|
||
selected={selectedPresetId === preset.id}
|
||
onSelect={onSelectPreset}
|
||
onFork={handleForkPreset}
|
||
onEdit={setEditingPreset}
|
||
onDelete={handleDeletePreset}
|
||
/>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div className="rounded-xl border border-dashed p-4 text-sm text-muted-foreground">
|
||
暂无公共风格化
|
||
</div>
|
||
)}
|
||
</section>
|
||
|
||
<section className="space-y-3">
|
||
<div className="text-sm font-medium">{privateTitle}</div>
|
||
{isLoading ? null : filteredPrivatePresets.length > 0 ? (
|
||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
||
{filteredPrivatePresets.map((preset) => (
|
||
<AgentPresetCard
|
||
key={preset.id}
|
||
preset={preset}
|
||
selected={selectedPresetId === preset.id}
|
||
onSelect={onSelectPreset}
|
||
onEdit={setEditingPreset}
|
||
onDelete={handleDeletePreset}
|
||
/>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div className="rounded-xl border border-dashed p-4 text-sm text-muted-foreground">
|
||
还没有私有风格化,先新建一个吧
|
||
</div>
|
||
)}
|
||
</section>
|
||
</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);
|
||
}}
|
||
/>
|
||
</>
|
||
);
|
||
}
|