ai-chatbot/components/agents/AgentPresetPanel.tsx
Your Name afc9e187b6
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
refactor: simplify agent preset setup
2026-03-17 22:11:36 +08:00

270 lines
9.6 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 { 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);
}}
/>
</>
);
}