"use client"; import Link from "next/link"; import { useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import useSWR from "swr"; import { toast } from "sonner"; import { motion, AnimatePresence } from "framer-motion"; import { ArrowLeft, ChevronDown, ChevronRight, GripVertical, Image as ImageIcon, Layers, Loader2, MonitorPlay, Package, Plus, Save, Search, Settings2, Sparkles, Trash2, Upload, UserCircle, Video, } from "lucide-react"; import { AGENT_WORKFLOW_MAX_TREE_DEPTH, countAgentWorkflowOptions, countAgentWorkflowOptionTree, countAgentWorkflowSteps, defaultAgentWorkflowUiConfig, findAgentWorkflowOptionByKey, findAgentWorkflowStepByKey, flattenAgentWorkflowOptionTree, getEnabledAgentWorkflowOptionTree, normalizeAgentWorkflowOptionOrdering, flattenAgentWorkflowSteps, normalizeAgentWorkflowStepOrdering, normalizeAgentWorkflowUiConfig, } from "@/lib/agents/workflow-ui-config"; import { defaultAgentWorkflowBrandModelConfig, normalizeAgentWorkflowBrandModelConfig, } from "@/lib/agents/workflow-brand-model-config"; import { AGENT_PRESET_LIBRARY_DEFAULT_SECTION_ID, AGENT_PRESET_LIBRARY_MAX_DEPTH, defaultAgentPresetLibraryConfig, normalizeAgentPresetLibraryConfig, } from "@/lib/agents/preset-library-config"; import { defaultAgentWorkflowShowcaseConfig, normalizeAgentWorkflowShowcaseConfig, } from "@/lib/agents/workflow-showcase-config"; import { defaultAgentWorkflowMaterialApiConfig, normalizeAgentWorkflowMaterialApiConfig, } from "@/lib/agents/workflow-material-config"; import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Switch } from "@/components/ui/switch"; import { Badge } from "@/components/ui/badge"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { Separator } from "@/components/ui/separator"; import { cn } from "@/lib/utils"; import type { AgentPresetLibraryCategoryNode, AgentPresetLibraryConfig, AgentPresetLibrarySection, AgentWorkflowBrandModelConfig, AgentWorkflowBrandStoreEntry, AgentWorkflowMaterialApiConfig, AgentWorkflowOptionUiConfig, AgentWorkflowShowcaseConfig, AgentWorkflowStepUiConfig, AgentWorkflowUiConfig, } from "@/types/agent"; const fetcher = (url: string) => fetch(url, { cache: "no-store" }).then((res) => res.json()); // Animation variants const containerVariants = { hidden: { opacity: 0 }, visible: { opacity: 1, transition: { staggerChildren: 0.05 } } }; const itemVariants = { hidden: { opacity: 0, y: 10 }, visible: { opacity: 1, y: 0 } }; const WORKFLOW_LEVEL_LABELS = ["一级模块", "二级模块", "三级模块"]; const WORKFLOW_OPTION_LEVEL_LABELS = ["一级卡片", "二级卡片", "三级卡片"]; const PRESET_LIBRARY_LEVEL_LABELS = ["一级分类", "二级分类", "三级分类"]; function normalizeConfigOrdering(config: AgentWorkflowUiConfig): AgentWorkflowUiConfig { return { ...config, steps: normalizeAgentWorkflowStepOrdering(config.steps), }; } function createWorkflowSlug(value: string, fallback: string) { const normalized = value .trim() .replace(/\s+/g, "_") .replace(/[^\w-]/g, "_") .replace(/_+/g, "_") .replace(/^_+|_+$/g, "") .toLowerCase(); return normalized || fallback; } function ensureUniqueWorkflowKey(baseKey: string, existingKeys: string[]) { const normalizedBase = createWorkflowSlug(baseKey, "item"); const keySet = new Set(existingKeys); if (!keySet.has(normalizedBase)) { return normalizedBase; } let suffix = 2; let candidate = `${normalizedBase}_${suffix}`; while (keySet.has(candidate)) { suffix += 1; candidate = `${normalizedBase}_${suffix}`; } return candidate; } function createWorkflowOptionDraft( stepKey: string, optionIndex: number, existingOptionKeys: string[] ): AgentWorkflowOptionUiConfig { const optionKey = ensureUniqueWorkflowKey( `${stepKey}_option_${optionIndex + 1}`, existingOptionKeys ); return { key: optionKey, buttonName: `选项${optionIndex + 1}`, description: "", previewImageUrl: "", enabled: true, sortOrder: optionIndex, promptFragment: `请按选项${optionIndex + 1}执行。`, children: [], }; } function createWorkflowStepDraft( depth: number, stepIndex: number, existingStepKeys: string[] ): AgentWorkflowStepUiConfig { const levelLabel = WORKFLOW_LEVEL_LABELS[Math.max(0, Math.min(depth - 1, 2))] || "模块"; const stepKey = ensureUniqueWorkflowKey(`step_${depth}_${stepIndex + 1}`, existingStepKeys); return { key: stepKey, title: `新${levelLabel}${stepIndex + 1}`, description: "", enabled: true, sortOrder: stepIndex, options: [createWorkflowOptionDraft(stepKey, 0, [])], children: [], }; } function updateWorkflowStepTree( steps: AgentWorkflowStepUiConfig[], stepKey: string, updater: (step: AgentWorkflowStepUiConfig) => AgentWorkflowStepUiConfig ): AgentWorkflowStepUiConfig[] { return steps.map((step) => { if (step.key === stepKey) { return updater(step); } return { ...step, children: updateWorkflowStepTree(step.children || [], stepKey, updater), }; }); } function collectWorkflowOptionKeys(options: AgentWorkflowOptionUiConfig[]): string[] { return flattenAgentWorkflowOptionTree(options).map(({ option }) => option.key); } function updateWorkflowOptionTree( options: AgentWorkflowOptionUiConfig[], optionKey: string, updater: (option: AgentWorkflowOptionUiConfig) => AgentWorkflowOptionUiConfig ): AgentWorkflowOptionUiConfig[] { return options.map((option) => { if (option.key === optionKey) { return updater(option); } return { ...option, children: updateWorkflowOptionTree(option.children || [], optionKey, updater), }; }); } function removeWorkflowOptionTree( options: AgentWorkflowOptionUiConfig[], optionKey: string ): AgentWorkflowOptionUiConfig[] { return options .filter((option) => option.key !== optionKey) .map((option) => ({ ...option, children: removeWorkflowOptionTree(option.children || [], optionKey), })); } function removeWorkflowStepTree( steps: AgentWorkflowStepUiConfig[], stepKey: string ): AgentWorkflowStepUiConfig[] { return steps .filter((step) => step.key !== stepKey) .map((step) => ({ ...step, children: removeWorkflowStepTree(step.children || [], stepKey), })); } function collectWorkflowStepKeys(steps: AgentWorkflowStepUiConfig[]): string[] { return flattenAgentWorkflowSteps(steps).map(({ step }) => step.key); } function collectDescendantStepKeys(step: AgentWorkflowStepUiConfig): string[] { return [step.key, ...collectWorkflowStepKeys(step.children || [])]; } function createPresetLibraryId(value: string, fallback: string) { const normalized = value .trim() .replace(/\s+/g, "_") .replace(/[^\w-]/g, "_") .replace(/_+/g, "_") .replace(/^_+|_+$/g, "") .toLowerCase(); return normalized || fallback; } function ensureUniquePresetLibraryId(baseId: string, existingIds: string[]) { const normalizedBase = createPresetLibraryId(baseId, "item"); const idSet = new Set(existingIds); if (!idSet.has(normalizedBase)) { return normalizedBase; } let suffix = 2; let candidate = `${normalizedBase}_${suffix}`; while (idSet.has(candidate)) { suffix += 1; candidate = `${normalizedBase}_${suffix}`; } return candidate; } function collectPresetLibraryNodeIds( nodes: AgentPresetLibraryCategoryNode[] ): string[] { return nodes.flatMap((node) => [node.id, ...collectPresetLibraryNodeIds(node.children || [])]); } function countPresetLibraryNodes(nodes: AgentPresetLibraryCategoryNode[]): number { return nodes.reduce( (total, node) => total + 1 + countPresetLibraryNodes(node.children || []), 0 ); } function updatePresetLibraryNodeTree( nodes: AgentPresetLibraryCategoryNode[], nodeId: string, updater: (node: AgentPresetLibraryCategoryNode) => AgentPresetLibraryCategoryNode ): AgentPresetLibraryCategoryNode[] { return nodes.map((node) => { if (node.id === nodeId) { return updater(node); } return { ...node, children: updatePresetLibraryNodeTree(node.children || [], nodeId, updater), }; }); } function removePresetLibraryNodeTree( nodes: AgentPresetLibraryCategoryNode[], nodeId: string ): AgentPresetLibraryCategoryNode[] { return nodes .filter((node) => node.id !== nodeId) .map((node) => ({ ...node, children: removePresetLibraryNodeTree(node.children || [], nodeId), })); } function findPresetLibraryNodeById( nodes: AgentPresetLibraryCategoryNode[], nodeId: string ): AgentPresetLibraryCategoryNode | null { for (const node of nodes) { if (node.id === nodeId) { return node; } const foundChild = findPresetLibraryNodeById(node.children || [], nodeId); if (foundChild) { return foundChild; } } return null; } function createPresetLibraryNodeDraft( depth: number, nodeIndex: number, existingIds: string[] ): AgentPresetLibraryCategoryNode { const levelLabel = PRESET_LIBRARY_LEVEL_LABELS[Math.max(0, Math.min(depth - 1, 2))] || "分类"; return { id: ensureUniquePresetLibraryId(`category_${depth}_${nodeIndex + 1}`, existingIds), name: `新${levelLabel}${nodeIndex + 1}`, enabled: true, sortOrder: nodeIndex, children: [], }; } function createPresetLibrarySectionDraft( sectionIndex: number, existingIds: string[] ): AgentPresetLibrarySection { const fallbackId = sectionIndex === 0 ? AGENT_PRESET_LIBRARY_DEFAULT_SECTION_ID : `section_${sectionIndex + 1}`; return { id: ensureUniquePresetLibraryId(fallbackId, existingIds), label: `分类页${sectionIndex + 1}`, description: "", enabled: true, sortOrder: sectionIndex, categories: [], }; } function buildWorkflowInputOptionsFromBrandEntries( entries: AgentWorkflowBrandStoreEntry[] ) { const normalizedEntries = entries.map((entry, index) => ({ brandName: entry.brandName.trim(), stores: Array.from( new Set( (entry.stores || []) .map((store) => store.trim()) .filter(Boolean) ) ), sortOrder: index, })); return { brandStoreEntries: normalizedEntries, brands: normalizedEntries.map((entry) => entry.brandName), stores: Array.from(new Set(normalizedEntries.flatMap((entry) => entry.stores))), }; } interface AgentWorkflowSettingsClientProps { backHref?: string; backLabel?: string; } export function AgentWorkflowSettingsClient({ backHref = "/settings", backLabel = "返回设置", }: AgentWorkflowSettingsClientProps = {}) { const { data, isLoading, mutate } = useSWR<{ agentPresetLibraryConfig?: AgentPresetLibraryConfig; agentWorkflowUiConfig?: AgentWorkflowUiConfig; agentWorkflowMaterialApiConfig?: AgentWorkflowMaterialApiConfig; agentWorkflowBrandModelConfig?: AgentWorkflowBrandModelConfig; agentWorkflowShowcaseConfig?: AgentWorkflowShowcaseConfig; }>("/api/site-settings", fetcher); const [presetLibraryConfig, setPresetLibraryConfig] = useState( defaultAgentPresetLibraryConfig ); const [config, setConfig] = useState(defaultAgentWorkflowUiConfig); const [materialApiConfig, setMaterialApiConfig] = useState( defaultAgentWorkflowMaterialApiConfig ); const [showcaseConfig, setShowcaseConfig] = useState( defaultAgentWorkflowShowcaseConfig ); const [brandModelConfig, setBrandModelConfig] = useState( defaultAgentWorkflowBrandModelConfig ); const [isSaving, setIsSaving] = useState(false); const [uploadingKey, setUploadingKey] = useState(null); const [expandedSteps, setExpandedSteps] = useState>(new Set()); const [activeTab, setActiveTab] = useState("showcase"); const [brandDraft, setBrandDraft] = useState(""); const [storeDrafts, setStoreDrafts] = useState>({}); const fileInputRefs = useRef>({}); useEffect(() => { if (!data) return; setPresetLibraryConfig( normalizeAgentPresetLibraryConfig(data.agentPresetLibraryConfig) ); const normalizedUiConfig = normalizeAgentWorkflowUiConfig(data.agentWorkflowUiConfig); const flattenedUiConfig = normalizeConfigOrdering({ ...normalizedUiConfig, steps: flattenAgentWorkflowSteps(normalizedUiConfig.steps).map(({ step }, index) => ({ ...step, sortOrder: index, children: [], })), }); setConfig(flattenedUiConfig); setMaterialApiConfig( normalizeAgentWorkflowMaterialApiConfig(data.agentWorkflowMaterialApiConfig) ); setShowcaseConfig( normalizeAgentWorkflowShowcaseConfig(data.agentWorkflowShowcaseConfig) ); setBrandModelConfig( normalizeAgentWorkflowBrandModelConfig(data.agentWorkflowBrandModelConfig) ); if (flattenedUiConfig.steps[0]) { setExpandedSteps(new Set([flattenedUiConfig.steps[0].key])); } }, [data]); const brandStoreEntries = useMemo( () => config.inputOptions?.brandStoreEntries || defaultAgentWorkflowUiConfig.inputOptions?.brandStoreEntries || [], [config.inputOptions?.brandStoreEntries] ); const configuredBrands = useMemo( () => brandStoreEntries.map((entry) => entry.brandName), [brandStoreEntries] ); const brandModelBrands = useMemo(() => { return Array.from( new Set([ ...configuredBrands, ...brandModelConfig.entries.map((entry) => entry.brandName), ]) ).sort((left, right) => left.localeCompare(right, "zh-CN")); }, [brandModelConfig.entries, configuredBrands]); const sortedSteps = useMemo( () => [...config.steps].sort((left, right) => left.sortOrder - right.sortOrder), [config.steps] ); const sortedPresetLibrarySections = useMemo( () => [...presetLibraryConfig.sections].sort((left, right) => left.sortOrder - right.sortOrder), [presetLibraryConfig.sections] ); const updateInputOptions = (nextEntries: AgentWorkflowBrandStoreEntry[]) => { setConfig((prev) => ({ ...prev, inputOptions: buildWorkflowInputOptionsFromBrandEntries(nextEntries), })); }; const normalizeInputOptionValue = (value: string) => value .replace(/\s+/g, " ") .trim(); const handleAddBrandEntry = () => { const nextValue = normalizeInputOptionValue(brandDraft); if (!nextValue) { toast.error("请输入品牌名称"); return; } if (brandStoreEntries.some((item) => item.brandName.toLowerCase() === nextValue.toLowerCase())) { toast.error("该品牌已存在"); return; } updateInputOptions([ ...brandStoreEntries, { brandName: nextValue, stores: [], sortOrder: brandStoreEntries.length, }, ]); setBrandDraft(""); }; const handleRemoveBrandEntry = (brandName: string) => { updateInputOptions(brandStoreEntries.filter((entry) => entry.brandName !== brandName)); setStoreDrafts((prev) => { const next = { ...prev }; delete next[brandName]; return next; }); }; const handleAddStoreToBrand = (brandName: string) => { const draft = storeDrafts[brandName] || ""; const nextValue = normalizeInputOptionValue(draft); if (!nextValue) { toast.error("请输入店铺名称"); return; } const targetEntry = brandStoreEntries.find((entry) => entry.brandName === brandName); if (targetEntry?.stores.some((store) => store.toLowerCase() === nextValue.toLowerCase())) { toast.error("该店铺已存在"); return; } updateInputOptions( brandStoreEntries.map((entry) => { if (entry.brandName !== brandName) return entry; return { ...entry, stores: [...entry.stores, nextValue], }; }) ); setStoreDrafts((prev) => ({ ...prev, [brandName]: "", })); }; const handleRemoveStoreFromBrand = (brandName: string, storeName: string) => { updateInputOptions( brandStoreEntries.map((entry) => entry.brandName === brandName ? { ...entry, stores: entry.stores.filter((store) => store !== storeName), } : entry ) ); }; const enabledStepsCount = useMemo( () => flattenAgentWorkflowSteps(sortedSteps).filter(({ step }) => step.enabled).length, [sortedSteps] ); const totalOptionsCount = useMemo( () => flattenAgentWorkflowSteps(sortedSteps).reduce( (acc, { step }) => acc + step.options.filter((option) => option.enabled).length, 0 ), [sortedSteps] ); const enabledPresetLibrarySectionCount = useMemo( () => sortedPresetLibrarySections.filter((section) => section.enabled).length, [sortedPresetLibrarySections] ); const totalPresetLibraryNodeCount = useMemo( () => sortedPresetLibrarySections.reduce( (total, section) => total + countPresetLibraryNodes(section.categories || []), 0 ), [sortedPresetLibrarySections] ); const updatePresetLibrarySection = ( sectionId: string, updater: (section: AgentPresetLibrarySection) => AgentPresetLibrarySection ) => { setPresetLibraryConfig((prev) => ({ sections: prev.sections .map((section) => (section.id === sectionId ? updater(section) : section)) .sort((left, right) => left.sortOrder - right.sortOrder) .map((section, index) => ({ ...section, sortOrder: index, })), })); }; const handleAddPresetLibrarySection = () => { setPresetLibraryConfig((prev) => { const nextSection = createPresetLibrarySectionDraft( prev.sections.length, prev.sections.map((section) => section.id) ); return { sections: [...prev.sections, nextSection].map((section, index) => ({ ...section, sortOrder: index, })), }; }); setActiveTab("categories"); }; const handleRemovePresetLibrarySection = (sectionId: string) => { setPresetLibraryConfig((prev) => ({ sections: prev.sections .filter((section) => section.id !== sectionId) .map((section, index) => ({ ...section, sortOrder: index, })), })); }; const handleAddPresetLibraryNode = (sectionId: string, parentNodeId?: string) => { setPresetLibraryConfig((prev) => { const targetSection = prev.sections.find((section) => section.id === sectionId); if (!targetSection) return prev; const existingNodeIds = collectPresetLibraryNodeIds(targetSection.categories || []); const nextSections = prev.sections.map((section) => { if (section.id !== sectionId) return section; if (!parentNodeId) { return { ...section, categories: [ ...(section.categories || []), createPresetLibraryNodeDraft(1, (section.categories || []).length, existingNodeIds), ].map((node, index) => ({ ...node, sortOrder: index, })), }; } const resolveDepth = ( nodes: AgentPresetLibraryCategoryNode[], depth: number ): number | null => { for (const node of nodes) { if (node.id === parentNodeId) return depth; const childDepth = resolveDepth(node.children || [], depth + 1); if (childDepth) return childDepth; } return null; }; const parentNode = findPresetLibraryNodeById(section.categories || [], parentNodeId); const parentDepth = resolveDepth(section.categories || [], 1); if (!parentNode || !parentDepth || parentDepth >= AGENT_PRESET_LIBRARY_MAX_DEPTH) { return section; } return { ...section, categories: updatePresetLibraryNodeTree( section.categories || [], parentNodeId, (node) => ({ ...node, children: [ ...(node.children || []), createPresetLibraryNodeDraft( parentDepth + 1, (node.children || []).length, existingNodeIds ), ].map((child, index) => ({ ...child, sortOrder: index, })), }) ), }; }); return { sections: nextSections }; }); setActiveTab("categories"); }; const handleRemovePresetLibraryNode = (sectionId: string, nodeId: string) => { setPresetLibraryConfig((prev) => ({ sections: prev.sections.map((section) => { if (section.id !== sectionId) return section; return { ...section, categories: removePresetLibraryNodeTree(section.categories || [], nodeId).map( (node, index) => ({ ...node, sortOrder: index, }) ), }; }), })); }; const updatePresetLibraryNode = ( sectionId: string, nodeId: string, updater: (node: AgentPresetLibraryCategoryNode) => AgentPresetLibraryCategoryNode ) => { setPresetLibraryConfig((prev) => ({ sections: prev.sections.map((section) => { if (section.id !== sectionId) return section; return { ...section, categories: updatePresetLibraryNodeTree(section.categories || [], nodeId, updater), }; }), })); }; const toggleStepExpansion = (stepKey: string) => { setExpandedSteps((prev) => { const next = new Set(prev); if (next.has(stepKey)) { next.delete(stepKey); } else { next.add(stepKey); } return next; }); }; const updateStep = ( stepKey: string, updater: (step: AgentWorkflowUiConfig["steps"][number]) => AgentWorkflowUiConfig["steps"][number] ) => { setConfig((prev) => normalizeConfigOrdering({ ...prev, steps: updateWorkflowStepTree(prev.steps, stepKey, updater), }) ); }; const updateOption = ( stepKey: string, optionKey: string, updater: ( option: AgentWorkflowUiConfig["steps"][number]["options"][number] ) => AgentWorkflowUiConfig["steps"][number]["options"][number] ) => { updateStep(stepKey, (step) => ({ ...step, options: updateWorkflowOptionTree(step.options, optionKey, updater), })); }; const handleAddStep = () => { setConfig((prev) => { const existingStepKeys = collectWorkflowStepKeys(prev.steps); const newStep = createWorkflowStepDraft(1, prev.steps.length, existingStepKeys); setExpandedSteps((prevExpanded) => new Set([...prevExpanded, newStep.key])); return normalizeConfigOrdering({ ...prev, steps: [...prev.steps, newStep], }); }); setActiveTab("workflow"); }; const handleRemoveStep = (stepKey: string) => { const targetStep = findAgentWorkflowStepByKey(config.steps, stepKey); setConfig((prev) => normalizeConfigOrdering({ ...prev, steps: removeWorkflowStepTree(prev.steps, stepKey), }) ); setExpandedSteps((prev) => { const next = new Set(prev); for (const key of targetStep ? collectDescendantStepKeys(targetStep) : [stepKey]) { next.delete(key); } return next; }); }; const handleAddOption = (stepKey: string, parentOptionKey?: string) => { updateStep(stepKey, (step) => { const existingOptionKeys = collectWorkflowOptionKeys(step.options || []); if (!parentOptionKey) { return { ...step, options: normalizeAgentWorkflowOptionOrdering([ ...step.options, createWorkflowOptionDraft(step.key, step.options.length, existingOptionKeys), ]), }; } const parentOption = findAgentWorkflowOptionByKey(step.options || [], parentOptionKey); const parentDepth = flattenAgentWorkflowOptionTree(step.options || []).find( ({ option }) => option.key === parentOptionKey )?.depth; if (!parentOption || !parentDepth || parentDepth >= 3) { return step; } return { ...step, options: updateWorkflowOptionTree(step.options || [], parentOptionKey, (option) => ({ ...option, children: normalizeAgentWorkflowOptionOrdering([ ...(option.children || []), createWorkflowOptionDraft( step.key, (option.children || []).length, existingOptionKeys ), ]), })), }; }); }; const handleRemoveOption = (stepKey: string, optionKey: string) => { updateStep(stepKey, (step) => ({ ...step, options: normalizeAgentWorkflowOptionOrdering( removeWorkflowOptionTree(step.options, optionKey) ), })); }; const handleUploadPreview = async (stepKey: string, optionKey: string, file: File) => { if (!file.type.startsWith("image/")) { toast.error("请上传图片文件"); return; } const refKey = `${stepKey}:${optionKey}`; try { setUploadingKey(refKey); const formData = new FormData(); formData.append("file", file); const response = await fetchUploadWithRetry(formData); const result = await response.json(); if (!response.ok || !result?.url) { throw new Error(result?.error || "上传失败"); } updateOption(stepKey, optionKey, (option) => ({ ...option, previewImageUrl: result.url, })); toast.success("预览图上传成功"); } catch (error) { toast.error(error instanceof Error ? error.message : "预览图上传失败"); } finally { setUploadingKey(null); } }; const handleSave = async () => { try { setIsSaving(true); const response = await fetch("/api/site-settings", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ agentPresetLibraryConfig: presetLibraryConfig, agentWorkflowUiConfig: config, agentWorkflowMaterialApiConfig: materialApiConfig, agentWorkflowBrandModelConfig: brandModelConfig, agentWorkflowShowcaseConfig: showcaseConfig, }), }); if (!response.ok) { throw new Error("保存失败"); } toast.success("智能体工作流配置已保存"); await mutate(); } catch (error) { toast.error(error instanceof Error ? error.message : "保存失败"); } finally { setIsSaving(false); } }; const renderPresetLibraryNodeCard = ( sectionId: string, node: AgentPresetLibraryCategoryNode, depth: number, indexPath: number[] ): React.ReactNode => { const levelLabel = PRESET_LIBRARY_LEVEL_LABELS[Math.max(0, Math.min(depth - 1, PRESET_LIBRARY_LEVEL_LABELS.length - 1))] || `第 ${depth} 级分类`; const canAddChild = depth < AGENT_PRESET_LIBRARY_MAX_DEPTH; const sortedChildren = [...(node.children || [])].sort( (left, right) => left.sortOrder - right.sortOrder ); return (
1 && "pl-4 sm:pl-6")}> 1 && "border-l-4 border-l-sky-500/30")}>
{indexPath.join(".")}
updatePresetLibraryNode(sectionId, node.id, (currentNode) => ({ ...currentNode, name: e.target.value, })) } className="h-8 min-w-[200px] max-w-[320px] border-0 bg-transparent px-0 text-base font-medium shadow-none focus-visible:ring-0" placeholder="分类名称" /> {levelLabel} {node.id}
{sortedChildren.length} 子分类 {canAddChild ? ( ) : ( 已达三级 )} updatePresetLibraryNode(sectionId, node.id, (currentNode) => ({ ...currentNode, enabled: checked, })) } />
{sortedChildren.length > 0 ? (
{sortedChildren.map((childNode, childIndex) => renderPresetLibraryNodeCard(sectionId, childNode, depth + 1, [ ...indexPath, childIndex + 1, ]) )}
) : (
暂无子分类
)}
); }; const renderPresetLibrarySectionCard = ( section: AgentPresetLibrarySection, index: number ) => { const sortedCategories = [...(section.categories || [])].sort( (left, right) => left.sortOrder - right.sortOrder ); return (
{index + 1}
顶部分类页 “我的作品”固定保留,这里维护其余可显示的生图模式分类页与三级分类树。
updatePresetLibrarySection(section.id, (currentSection) => ({ ...currentSection, label: e.target.value, })) } placeholder="例如:案例广场" />
updatePresetLibrarySection(section.id, (currentSection) => ({ ...currentSection, description: e.target.value, })) } placeholder="用于后台区分该分类页的用途" />
{sortedCategories.length} 个一级分类 共 {countPresetLibraryNodes(section.categories || [])} 个树节点 {section.id}
updatePresetLibrarySection(section.id, (currentSection) => ({ ...currentSection, enabled: checked, })) } />
{sortedCategories.length > 0 ? (
{sortedCategories.map((node, nodeIndex) => renderPresetLibraryNodeCard(section.id, node, 1, [nodeIndex + 1]) )}
) : (
当前分类页还没有任何分类树节点,先新增一级分类即可继续建立二级、三级关系。
)}
); }; const renderWorkflowOptionCard = ( step: AgentWorkflowStepUiConfig, option: AgentWorkflowOptionUiConfig, depth: number, indexPath: number[] ): ReactNode => { const refKey = `${step.key}:${option.key}`; const isUploading = uploadingKey === refKey; const levelLabel = WORKFLOW_OPTION_LEVEL_LABELS[Math.max(0, Math.min(depth - 1, WORKFLOW_OPTION_LEVEL_LABELS.length - 1))] || `第 ${depth} 级卡片`; const sortedChildren = [...(option.children || [])].sort( (left, right) => left.sortOrder - right.sortOrder ); const canAddChild = depth < 3; return (
1 && "pl-4 sm:pl-6")}>
1 && "border-l-4 border-l-sky-500/30" )} >
{indexPath.join(".")} {levelLabel} {sortedChildren.length} 子卡片 {option.key}
{option.previewImageUrl ? ( {option.buttonName} ) : (
无图片
)}
{ fileInputRefs.current[refKey] = el; }} type="file" accept="image/*" className="hidden" onChange={(e) => { const file = e.target.files?.[0]; if (file) { void handleUploadPreview(step.key, option.key, file); } e.target.value = ""; }} />
updateOption(step.key, option.key, (currentOption) => ({ ...currentOption, buttonName: e.target.value, })) } placeholder="选项名称" className="h-8" />
updateOption(step.key, option.key, (currentOption) => ({ ...currentOption, description: e.target.value, })) } placeholder="简短描述" className="h-8" />