"use client"; import { useEffect, useMemo, useRef, useState } from "react"; import useSWR from "swr"; import { Loader2, Plus, Trash2, Upload } from "lucide-react"; import { toast } from "sonner"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { ScrollArea } from "@/components/ui/scroll-area"; import { fetcher } from "@/lib/utils"; import { imageGenerationService } from "@/services/canvas"; import { agentService } from "@/services/agents"; import type { AgentCategory, AgentPreset, AgentPresetSlot } from "@/types/agent"; type DialogSlotState = AgentPresetSlot & { localReferenceAssets: Array<{ assetId: string; url: string; name?: string | null; }>; }; interface AgentPresetDialogProps { open: boolean; onOpenChange: (open: boolean) => void; categories: AgentCategory[]; initialPreset?: AgentPreset | null; onSaved?: (preset: AgentPreset) => void; } const NEW_CATEGORY_VALUE = "__new__"; export function AgentPresetDialog({ open, onOpenChange, categories, initialPreset, onSaved, }: AgentPresetDialogProps) { const { data: modelsData } = useSWR<{ models?: Array<{ id: string; displayName: string; supportsImageGen?: boolean; providerType?: string; }>; }>("/api/models", fetcher); const [name, setName] = useState(""); const [type, setType] = useState<"image" | "video">("image"); const [visibility, setVisibility] = useState<"public" | "private">("private"); const [categoryMode, setCategoryMode] = useState(""); const [newCategoryName, setNewCategoryName] = useState(""); const [defaultModelId, setDefaultModelId] = useState(""); const [description, setDescription] = useState(""); const [avatarAssetId, setAvatarAssetId] = useState(""); const [avatarUrl, setAvatarUrl] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); const [uploadingSlotIndex, setUploadingSlotIndex] = useState(null); const [avatarUploading, setAvatarUploading] = useState(false); const [slots, setSlots] = useState([ { slotIndex: 0, title: "第1张", promptTemplate: "", requireUpload: true, uploadLabel: "第1张", defaultParams: null, referenceAssetIds: [], localReferenceAssets: [], }, ]); const fileInputRefs = useRef>([]); const avatarInputRef = useRef(null); const videoProviderTypes = useMemo( () => new Set(["volcano-video", "doubao-video", "qianwen-video", "fal-video", "ruxa-video"]), [] ); const filteredModels = useMemo(() => { const models = modelsData?.models || []; if (type === "video") { return models.filter((model) => videoProviderTypes.has(String(model.providerType || ""))); } return models.filter( (model) => model.supportsImageGen && !videoProviderTypes.has(String(model.providerType || "")) ); }, [modelsData?.models, type, videoProviderTypes]); useEffect(() => { if (!open) return; if (filteredModels.length === 0) { setDefaultModelId(""); return; } if (!filteredModels.some((model) => model.id === defaultModelId)) { setDefaultModelId(filteredModels[0]?.id || ""); } }, [defaultModelId, filteredModels, open]); useEffect(() => { if (!open) return; setSlots((current) => current.map((slot, index) => ({ ...slot, slotIndex: index, title: slot.title || `第${index + 1}张`, uploadLabel: slot.uploadLabel || `第${index + 1}张`, })) ); }, [open]); const resetState = () => { setName(""); setType("image"); setVisibility("private"); setCategoryMode(""); setNewCategoryName(""); setDefaultModelId(""); setDescription(""); setAvatarAssetId(""); setAvatarUrl(""); setSlots([ { slotIndex: 0, title: "第1张", promptTemplate: "", requireUpload: true, uploadLabel: "第1张", defaultParams: null, referenceAssetIds: [], localReferenceAssets: [], }, ]); setUploadingSlotIndex(null); }; useEffect(() => { if (!open) return; if (!initialPreset) { resetState(); return; } setName(initialPreset.name || ""); setType(initialPreset.type); setVisibility(initialPreset.visibility); setCategoryMode(initialPreset.category?.id || initialPreset.categoryId || ""); setNewCategoryName(""); setDefaultModelId(initialPreset.defaultModelId || ""); setDescription(initialPreset.description || ""); setAvatarAssetId(initialPreset.avatarAssetId || ""); setAvatarUrl(initialPreset.avatarUrl || ""); setSlots( (initialPreset.slots || []).map((slot, index) => ({ slotIndex: index, title: slot.title || `第${index + 1}张`, promptTemplate: slot.promptTemplate || "", requireUpload: slot.requireUpload !== false, uploadLabel: slot.uploadLabel || `第${index + 1}张`, defaultParams: slot.defaultParams || null, referenceAssetIds: slot.referenceAssetIds || [], localReferenceAssets: (slot.referenceAssets || []).map((asset) => ({ assetId: asset.assetId, url: asset.thumbnailUrl || asset.previewUrl || asset.originalUrl, name: asset.displayName, })), })) ); }, [initialPreset, open]); const handleSlotChange = (slotIndex: number, updates: Partial) => { setSlots((current) => current.map((slot, index) => index === slotIndex ? { ...slot, ...updates, slotIndex: index } : slot ) ); }; const handleAddSlot = () => { setSlots((current) => [ ...current, { slotIndex: current.length, title: `第${current.length + 1}张`, promptTemplate: "", requireUpload: true, uploadLabel: `第${current.length + 1}张`, defaultParams: null, referenceAssetIds: [], localReferenceAssets: [], }, ]); }; const handleRemoveSlot = (slotIndex: number) => { setSlots((current) => current .filter((_, index) => index !== slotIndex) .map((slot, index) => ({ ...slot, slotIndex: index, title: slot.title || `第${index + 1}张`, uploadLabel: slot.uploadLabel || `第${index + 1}张`, })) ); }; const handleAvatarUpload = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; setAvatarUploading(true); try { const result = await imageGenerationService.uploadImage(file); if (!result.success || !result.data?.assetId || !result.data.url) { throw new Error(result.error || "头像上传失败"); } setAvatarAssetId(result.data.assetId); setAvatarUrl(result.data.url); toast.success("头像上传成功"); } catch (error) { toast.error(error instanceof Error ? error.message : "头像上传失败"); } finally { setAvatarUploading(false); event.target.value = ""; } }; const handleUploadReferenceImages = async ( slotIndex: number, event: React.ChangeEvent ) => { const files = Array.from(event.target.files || []); if (files.length === 0) return; setUploadingSlotIndex(slotIndex); try { const uploadedAssets: Array<{ assetId: string; url: string; name?: string | null }> = []; for (const file of files) { const result = await imageGenerationService.uploadImage(file); if (!result.success || !result.data?.assetId || !result.data.url) { throw new Error(result.error || "参考图上传失败"); } uploadedAssets.push({ assetId: result.data.assetId, url: result.data.url, name: file.name, }); } setSlots((current) => current.map((slot, index) => { if (index !== slotIndex) return slot; const nextAssets = [...slot.localReferenceAssets, ...uploadedAssets]; return { ...slot, referenceAssetIds: nextAssets.map((item) => item.assetId), localReferenceAssets: nextAssets, }; }) ); toast.success( uploadedAssets.length === 1 ? "参考图上传成功" : `已上传 ${uploadedAssets.length} 张参考图` ); } catch (error) { toast.error(error instanceof Error ? error.message : "参考图上传失败"); } finally { setUploadingSlotIndex(null); event.target.value = ""; } }; const handleCreate = async () => { if (!name.trim()) { toast.error("请输入智能体名称"); return; } if (!defaultModelId) { toast.error("请选择对应模型"); return; } if (visibility === "public" && !categoryMode && !newCategoryName.trim()) { toast.error("公开智能体必须选择分类"); return; } if (slots.some((slot) => !slot.promptTemplate.trim())) { toast.error("每个槽位都需要填写提示词"); return; } setIsSubmitting(true); try { const payload = { name: name.trim(), type, visibility, categoryId: categoryMode && categoryMode !== NEW_CATEGORY_VALUE ? categoryMode : undefined, newCategoryName: categoryMode === NEW_CATEGORY_VALUE ? newCategoryName.trim() : undefined, description: description.trim() || undefined, defaultModelId, slots: slots.map((slot, index) => ({ slotIndex: index, title: slot.title || null, promptTemplate: slot.promptTemplate.trim(), requireUpload: slot.requireUpload !== false, uploadLabel: slot.uploadLabel || null, defaultParams: slot.defaultParams || undefined, referenceAssetIds: slot.referenceAssetIds || [], })), avatarAssetId: avatarAssetId || undefined, } as const; const result = initialPreset ? await agentService.updatePreset(initialPreset.id, payload) : await agentService.createPreset(payload); if (!result.success || !result.data) { throw new Error(result.error || (initialPreset ? "更新智能体失败" : "创建智能体失败")); } toast.success(initialPreset ? "智能体更新成功" : "智能体创建成功"); onSaved?.(result.data); onOpenChange(false); resetState(); } catch (error) { toast.error(error instanceof Error ? error.message : "创建智能体失败"); } finally { setIsSubmitting(false); } }; return ( { onOpenChange(nextOpen); if (!nextOpen) resetState(); }} > {initialPreset ? "编辑智能体" : "新建智能体"} 配置模型、槽位提示词和参考图。
预览图
{avatarUrl ? ( 智能体预览图 ) : (
未上传
)}
智能体名称
setName(event.target.value)} />
可见性
类型
分类
如需创建分类,请先选择“新建分类”,再在下方填写分类名称。
对应模型
{categoryMode === NEW_CATEGORY_VALUE ? (
新分类名称
setNewCategoryName(event.target.value)} />
) : null}
描述