ai-chatbot/components/agents/AgentPresetPanel.tsx
Your Name 4b820f8ff4
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
Add canvas agent mode and management
2026-03-10 19:56:25 +08:00

209 lines
7.4 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 useSWR from "swr";
import { Loader2, Plus, Sparkles } from "lucide-react";
import { toast } from "sonner";
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 { AgentCategoryTabs } from "@/components/agents/AgentCategoryTabs";
import { AgentPresetCard } from "@/components/agents/AgentPresetCard";
import { AgentPresetDialog } from "@/components/agents/AgentPresetDialog";
interface AgentPresetPanelProps {
selectedPresetId?: string | null;
onSelectPreset: (presetId: string | null) => void;
}
export function AgentPresetPanel({
selectedPresetId,
onSelectPreset,
}: AgentPresetPanelProps) {
const [keyword, setKeyword] = useState("");
const [selectedCategoryId, setSelectedCategoryId] = useState<string | null>(null);
const [showCreateDialog, setShowCreateDialog] = useState(false);
const [editingPreset, setEditingPreset] = useState<AgentPreset | null>(null);
const { data, isLoading, mutate } = useSWR(
["agent-presets", selectedCategoryId],
async () => {
const result = await agentService.getPresets({
scope: "all",
sort: "latest",
});
if (!result.success || !result.data) {
throw new Error(result.error || "获取智能体失败");
}
return result.data;
}
);
const publicCategories = useMemo(
() => (data?.categories || []).filter((category) => category.scope !== "private"),
[data?.categories]
);
const filteredPublicPresets = useMemo(() => {
return (data?.public || []).filter((preset) => {
const matchedCategory = !selectedCategoryId || preset.category?.id === selectedCategoryId;
const matchedKeyword =
!keyword.trim() ||
preset.name.toLowerCase().includes(keyword.trim().toLowerCase()) ||
String(preset.category?.name || "")
.toLowerCase()
.includes(keyword.trim().toLowerCase());
return matchedCategory && matchedKeyword;
});
}, [data?.public, keyword, selectedCategoryId]);
const filteredPrivatePresets = useMemo(() => {
return (data?.private || []).filter((preset) => {
const matchedKeyword =
!keyword.trim() ||
preset.name.toLowerCase().includes(keyword.trim().toLowerCase()) ||
String(preset.category?.name || "")
.toLowerCase()
.includes(keyword.trim().toLowerCase());
return matchedKeyword;
});
}, [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="flex h-full flex-col border-r bg-white dark:bg-zinc-950">
<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" />
智能体
</div>
<div className="mt-1 text-xs text-muted-foreground">
选择智能体后,右侧对话框会按其要求显示上传槽位
</div>
</div>
<Button size="sm" onClick={() => setShowCreateDialog(true)}>
<Plus className="mr-1 h-4 w-4" />
新建
</Button>
</div>
<Input
placeholder="搜索智能体"
value={keyword}
onChange={(event) => setKeyword(event.target.value)}
/>
</div>
<ScrollArea className="flex-1">
<div className="space-y-5 p-4">
<section className="space-y-3">
<div className="space-y-2">
<div className="text-sm font-medium">公共智能体</div>
<AgentCategoryTabs
categories={publicCategories}
selectedCategoryId={selectedCategoryId}
onSelectCategory={setSelectedCategoryId}
/>
</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="space-y-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">我的智能体</div>
{isLoading ? null : filteredPrivatePresets.length > 0 ? (
<div className="space-y-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>
<AgentPresetDialog
open={showCreateDialog || Boolean(editingPreset)}
onOpenChange={(nextOpen) => {
if (!nextOpen) {
setShowCreateDialog(false);
setEditingPreset(null);
return;
}
setShowCreateDialog(true);
}}
categories={data?.categories || []}
initialPreset={editingPreset}
onSaved={async (preset) => {
await mutate();
onSelectPreset(preset.id);
setEditingPreset(null);
setShowCreateDialog(false);
}}
/>
</>
);
}