209 lines
7.4 KiB
TypeScript
209 lines
7.4 KiB
TypeScript
"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);
|
||
}}
|
||
/>
|
||
</>
|
||
);
|
||
}
|