"use client"; import { FileText, ImagePlus, Layers, MonitorSmartphone, MousePointer2, PencilLine, Settings2, Sparkles, Video, } from "lucide-react"; import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import type { AgentSlotNodeData, CanvasNode } from "@/types/canvas"; import { cn } from "@/lib/utils"; interface AgentSlotNodeProps { node: CanvasNode; isSelected: boolean; onDataChange?: (updates: Partial) => void; } type SlotType = AgentSlotNodeData["slotType"]; type SlotFillPolicy = NonNullable; type SlotAcceptedSource = NonNullable[number]; const SLOT_TYPE_OPTIONS: Array<{ value: SlotType; label: string; icon: typeof ImagePlus; }> = [ { value: "image", label: "图片槽位", icon: ImagePlus }, { value: "video", label: "视频槽位", icon: Video }, { value: "ui-screen", label: "UI 槽位", icon: MonitorSmartphone }, ]; const FILL_POLICY_OPTIONS: Array<{ value: SlotFillPolicy; label: string; }> = [ { value: "replace-empty", label: "填充空槽" }, { value: "append-version", label: "追加版本" }, { value: "replace-existing", label: "允许替换" }, ]; const ACCEPTED_SOURCE_OPTIONS: Array<{ value: SlotAcceptedSource; label: string; icon: typeof FileText; }> = [ { value: "text", label: "文本", icon: FileText }, { value: "image", label: "图片", icon: ImagePlus }, { value: "selection", label: "选中对象", icon: MousePointer2 }, { value: "annotation", label: "标注", icon: PencilLine }, ]; const ASPECT_RATIO_OPTIONS = ["1:1", "4:3", "3:4", "16:9", "9:16"]; function getSlotIcon(slotType: AgentSlotNodeData["slotType"]) { if (slotType === "video") return Video; if (slotType === "ui-screen") return MonitorSmartphone; return ImagePlus; } function getSlotTypeLabel(slotType: AgentSlotNodeData["slotType"]) { if (slotType === "video") return "视频槽位"; if (slotType === "ui-screen") return "UI 槽位"; return "图片槽位"; } function getStatusLabel(status: AgentSlotNodeData["status"]) { if (status === "generating") return "生成中"; if (status === "completed") return "已填充"; if (status === "failed") return "失败"; return "等待生成"; } function getFillPolicyLabel(fillPolicy: AgentSlotNodeData["fillPolicy"]) { if (fillPolicy === "append-version") return "追加版本"; if (fillPolicy === "replace-existing") return "允许替换"; return "填充空槽"; } function normalizeAcceptedSources( acceptedSources: AgentSlotNodeData["acceptedSources"] ): SlotAcceptedSource[] { const normalized = (Array.isArray(acceptedSources) ? acceptedSources : []) .filter((value): value is SlotAcceptedSource => ACCEPTED_SOURCE_OPTIONS.some((option) => option.value === value) ); if (normalized.length > 0) { return Array.from(new Set(normalized)); } return ACCEPTED_SOURCE_OPTIONS.map((option) => option.value); } function getAcceptedSourcesLabel(acceptedSources: SlotAcceptedSource[]) { return ACCEPTED_SOURCE_OPTIONS.filter((option) => acceptedSources.includes(option.value) ) .map((option) => option.label) .join("、"); } function isSlotType(value: string): value is SlotType { return SLOT_TYPE_OPTIONS.some((option) => option.value === value); } function isSlotFillPolicy(value: string): value is SlotFillPolicy { return FILL_POLICY_OPTIONS.some((option) => option.value === value); } export function AgentSlotNode({ node, isSelected, onDataChange, }: AgentSlotNodeProps) { const data = node.data as AgentSlotNodeData; const slotType = data.slotType || "image"; const acceptedSources = normalizeAcceptedSources(data.acceptedSources); const SlotIcon = getSlotIcon(slotType); const filledCount = Array.isArray(data.filledNodeIds) ? data.filledNodeIds.length : 0; const fillPolicy = data.fillPolicy || "replace-empty"; const aspectRatio = data.aspectRatio || "1:1"; const canEdit = Boolean(onDataChange); const updateSlotData = (updates: Partial) => { if (!onDataChange) return; onDataChange({ ...updates, metadata: { ...(data.metadata || {}), ...(updates.metadata || {}), agentSlotConfiguredAt: new Date().toISOString(), }, }); }; const toggleAcceptedSource = ( source: SlotAcceptedSource, checked: boolean ) => { const next = checked ? Array.from(new Set([...acceptedSources, source])) : acceptedSources.filter((value) => value !== source); if (next.length === 0) return; updateSlotData({ acceptedSources: next }); }; return (
{canEdit ? ( event.stopPropagation()} onMouseDown={(event) => event.stopPropagation()} sideOffset={8} >
updateSlotData({ label: event.target.value }) } onClick={(event) => event.stopPropagation()} onKeyDown={(event) => event.stopPropagation()} onMouseDown={(event) => event.stopPropagation()} />
槽位类型 { if (!isSlotType(value)) return; updateSlotData({ slotType: value }); }} > {SLOT_TYPE_OPTIONS.map((option) => { const OptionIcon = option.icon; return ( {option.label} ); })} 画幅 { if (!ASPECT_RATIO_OPTIONS.includes(value)) return; updateSlotData({ aspectRatio: value }); }} > {ASPECT_RATIO_OPTIONS.map((option) => ( {option} ))} 填充策略 { if (!isSlotFillPolicy(value)) return; updateSlotData({ fillPolicy: value }); }} > {FILL_POLICY_OPTIONS.map((option) => ( {option.label} ))} 可用来源 {ACCEPTED_SOURCE_OPTIONS.map((option) => { const OptionIcon = option.icon; const checked = acceptedSources.includes(option.value); return ( toggleAcceptedSource(option.value, Boolean(nextChecked)) } onSelect={(event) => event.preventDefault()} > {option.label} ); })}
) : null}
{data.status === "generating" ? ( ) : ( )}
{data.label || getSlotTypeLabel(slotType)}
{getStatusLabel(data.status)} {filledCount > 0 ? ` · ${filledCount} 个结果` : ""}
{aspectRatio} {getAcceptedSourcesLabel(acceptedSources)} {getFillPolicyLabel(fillPolicy)}
); }