From 8d2066a3bfae250fa1e9836236cea2b4e9cb07e9 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 19 Mar 2026 18:33:42 +0800 Subject: [PATCH] feat: expand agent workflow and project management --- .../agent-workflow-settings-client.tsx | 1420 ++++++++++++----- app/api/agents/[id]/route.ts | 32 +- app/api/agents/route.ts | 20 + app/api/site-settings/route.ts | 151 +- app/canvas/[id]/page.tsx | 120 +- .../agents/AgentGenerationWorkspaceDarkV2.tsx | 366 +++-- components/agents/AgentPresetCardDark.tsx | 54 +- components/agents/AgentPresetDialog.tsx | 301 +++- components/agents/AgentPresetPanelDarkV2.tsx | 370 ++++- components/agents/AgentProjectListSheet.tsx | 55 +- components/agents/AgentWorkflowPanel.tsx | 28 +- components/agents/AgentWorkflowPanelDark.tsx | 29 +- .../agents/AgentWorkflowPanelDarkV2.tsx | 134 +- components/agents/AgentWorkflowPanelNew.tsx | 29 +- components/scene-fusion-view.tsx | 2 +- components/ui/searchable-select.tsx | 5 + components/ui/select.tsx | 12 +- lib/agents/preset-library-config.ts | 335 ++++ lib/agents/workflow-composer.ts | 25 +- lib/agents/workflow-ui-config.ts | 326 +++- lib/db/queries.ts | 16 + lib/db/schema.ts | 8 + types/agent.ts | 37 + 23 files changed, 3098 insertions(+), 777 deletions(-) create mode 100644 lib/agents/preset-library-config.ts diff --git a/app/(admin)/settings/agent-workflow/agent-workflow-settings-client.tsx b/app/(admin)/settings/agent-workflow/agent-workflow-settings-client.tsx index 574a536c7..560e76743 100644 --- a/app/(admin)/settings/agent-workflow/agent-workflow-settings-client.tsx +++ b/app/(admin)/settings/agent-workflow/agent-workflow-settings-client.tsx @@ -1,7 +1,7 @@ "use client"; import Link from "next/link"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import useSWR from "swr"; import { toast } from "sonner"; import { motion, AnimatePresence } from "framer-motion"; @@ -28,9 +28,14 @@ import { import { AGENT_WORKFLOW_MAX_TREE_DEPTH, countAgentWorkflowOptions, + countAgentWorkflowOptionTree, countAgentWorkflowSteps, defaultAgentWorkflowUiConfig, + findAgentWorkflowOptionByKey, findAgentWorkflowStepByKey, + flattenAgentWorkflowOptionTree, + getEnabledAgentWorkflowOptionTree, + normalizeAgentWorkflowOptionOrdering, flattenAgentWorkflowSteps, normalizeAgentWorkflowStepOrdering, normalizeAgentWorkflowUiConfig, @@ -39,6 +44,12 @@ 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, @@ -60,7 +71,11 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/component import { Separator } from "@/components/ui/separator"; import { cn } from "@/lib/utils"; import type { + AgentPresetLibraryCategoryNode, + AgentPresetLibraryConfig, + AgentPresetLibrarySection, AgentWorkflowBrandModelConfig, + AgentWorkflowBrandStoreEntry, AgentWorkflowMaterialApiConfig, AgentWorkflowOptionUiConfig, AgentWorkflowShowcaseConfig, @@ -85,6 +100,8 @@ const itemVariants = { }; const WORKFLOW_LEVEL_LABELS = ["一级模块", "二级模块", "三级模块"]; +const WORKFLOW_OPTION_LEVEL_LABELS = ["一级卡片", "二级卡片", "三级卡片"]; +const PRESET_LIBRARY_LEVEL_LABELS = ["一级分类", "二级分类", "三级分类"]; function normalizeConfigOrdering(config: AgentWorkflowUiConfig): AgentWorkflowUiConfig { return { @@ -137,6 +154,7 @@ function createWorkflowOptionDraft( enabled: true, sortOrder: optionIndex, promptFragment: `请按选项${optionIndex + 1}执行。`, + children: [], }; } @@ -174,6 +192,38 @@ function updateWorkflowStepTree( }); } +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 @@ -194,6 +244,145 @@ 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; @@ -204,11 +393,15 @@ export function AgentWorkflowSettingsClient({ 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 @@ -224,13 +417,24 @@ export function AgentWorkflowSettingsClient({ const [expandedSteps, setExpandedSteps] = useState>(new Set()); const [activeTab, setActiveTab] = useState("showcase"); const [brandDraft, setBrandDraft] = useState(""); - const [storeDraft, setStoreDraft] = useState(""); + const [storeDrafts, setStoreDrafts] = useState>({}); const fileInputRefs = useRef>({}); useEffect(() => { if (!data) return; + setPresetLibraryConfig( + normalizeAgentPresetLibraryConfig(data.agentPresetLibraryConfig) + ); const normalizedUiConfig = normalizeAgentWorkflowUiConfig(data.agentWorkflowUiConfig); - setConfig(normalizedUiConfig); + const flattenedUiConfig = normalizeConfigOrdering({ + ...normalizedUiConfig, + steps: flattenAgentWorkflowSteps(normalizedUiConfig.steps).map(({ step }, index) => ({ + ...step, + sortOrder: index, + children: [], + })), + }); + setConfig(flattenedUiConfig); setMaterialApiConfig( normalizeAgentWorkflowMaterialApiConfig(data.agentWorkflowMaterialApiConfig) ); @@ -240,14 +444,21 @@ export function AgentWorkflowSettingsClient({ setBrandModelConfig( normalizeAgentWorkflowBrandModelConfig(data.agentWorkflowBrandModelConfig) ); - if (normalizedUiConfig.steps[0]) { - setExpandedSteps(new Set([normalizedUiConfig.steps[0].key])); + 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( - () => config.inputOptions?.brands || defaultAgentWorkflowUiConfig.inputOptions?.brands || [], - [config.inputOptions?.brands] + () => brandStoreEntries.map((entry) => entry.brandName), + [brandStoreEntries] ); const brandModelBrands = useMemo(() => { return Array.from( @@ -262,16 +473,15 @@ export function AgentWorkflowSettingsClient({ () => [...config.steps].sort((left, right) => left.sortOrder - right.sortOrder), [config.steps] ); - const brandOptions = config.inputOptions?.brands || []; - const storeOptions = config.inputOptions?.stores || []; - - const updateInputOptions = (updates: Partial<{ brands: string[]; stores: string[] }>) => { + const sortedPresetLibrarySections = useMemo( + () => + [...presetLibraryConfig.sections].sort((left, right) => left.sortOrder - right.sortOrder), + [presetLibraryConfig.sections] + ); + const updateInputOptions = (nextEntries: AgentWorkflowBrandStoreEntry[]) => { setConfig((prev) => ({ ...prev, - inputOptions: { - brands: updates.brands ?? prev.inputOptions?.brands ?? [], - stores: updates.stores ?? prev.inputOptions?.stores ?? [], - }, + inputOptions: buildWorkflowInputOptionsFromBrandEntries(nextEntries), })); }; @@ -280,41 +490,78 @@ export function AgentWorkflowSettingsClient({ .replace(/\s+/g, " ") .trim(); - const handleAddInputOption = (kind: "brands" | "stores") => { - const draft = kind === "brands" ? brandDraft : storeDraft; - const nextValue = normalizeInputOptionValue(draft); + const handleAddBrandEntry = () => { + const nextValue = normalizeInputOptionValue(brandDraft); if (!nextValue) { - toast.error(kind === "brands" ? "请输入品牌名称" : "请输入店铺名称"); + toast.error("请输入品牌名称"); return; } - const currentList = kind === "brands" ? brandOptions : storeOptions; - if (currentList.some((item) => item.toLowerCase() === nextValue.toLowerCase())) { - toast.error(kind === "brands" ? "该品牌已存在" : "该店铺已存在"); + if (brandStoreEntries.some((item) => item.brandName.toLowerCase() === nextValue.toLowerCase())) { + toast.error("该品牌已存在"); return; } - if (kind === "brands") { - updateInputOptions({ brands: [...brandOptions, nextValue] }); - setBrandDraft(""); - return; - } - - updateInputOptions({ stores: [...storeOptions, nextValue] }); - setStoreDraft(""); + updateInputOptions([ + ...brandStoreEntries, + { + brandName: nextValue, + stores: [], + sortOrder: brandStoreEntries.length, + }, + ]); + setBrandDraft(""); }; - const handleRemoveInputOption = (kind: "brands" | "stores", value: string) => { - if (kind === "brands") { - updateInputOptions({ - brands: brandOptions.filter((item) => item !== value), - }); + 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; } - updateInputOptions({ - stores: storeOptions.filter((item) => item !== value), - }); + 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( @@ -329,6 +576,161 @@ export function AgentWorkflowSettingsClient({ ), [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) => { @@ -363,47 +765,18 @@ export function AgentWorkflowSettingsClient({ ) => { updateStep(stepKey, (step) => ({ ...step, - options: step.options.map((option) => - option.key === optionKey ? updater(option) : option - ), + options: updateWorkflowOptionTree(step.options, optionKey, updater), })); }; - const handleAddStep = (parentStepKey?: string) => { + const handleAddStep = () => { setConfig((prev) => { const existingStepKeys = collectWorkflowStepKeys(prev.steps); - - if (!parentStepKey) { - const newStep = createWorkflowStepDraft(1, prev.steps.length, existingStepKeys); - setExpandedSteps((prevExpanded) => new Set([...prevExpanded, newStep.key])); - return normalizeConfigOrdering({ - ...prev, - steps: [...prev.steps, newStep], - }); - } - - const parentStep = findAgentWorkflowStepByKey(prev.steps, parentStepKey); - const parentDepth = flattenAgentWorkflowSteps(prev.steps).find( - ({ step }) => step.key === parentStepKey - )?.depth; - if (!parentStep || !parentDepth || parentDepth >= AGENT_WORKFLOW_MAX_TREE_DEPTH) { - return prev; - } - - const newChildStep = createWorkflowStepDraft( - parentDepth + 1, - (parentStep.children || []).length, - existingStepKeys - ); - setExpandedSteps( - (prevExpanded) => new Set([...prevExpanded, parentStepKey, newChildStep.key]) - ); + const newStep = createWorkflowStepDraft(1, prev.steps.length, existingStepKeys); + setExpandedSteps((prevExpanded) => new Set([...prevExpanded, newStep.key])); return normalizeConfigOrdering({ ...prev, - steps: updateWorkflowStepTree(prev.steps, parentStepKey, (step) => ({ - ...step, - children: [...(step.children || []), newChildStep], - })), + steps: [...prev.steps, newStep], }); }); setActiveTab("workflow"); @@ -426,20 +799,50 @@ export function AgentWorkflowSettingsClient({ }); }; - const handleAddOption = (stepKey: string) => { - updateStep(stepKey, (step) => ({ - ...step, - options: [ - ...step.options, - createWorkflowOptionDraft(step.key, step.options.length, step.options.map((item) => item.key)), - ], - })); + 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: step.options.filter((option) => option.key !== optionKey), + options: normalizeAgentWorkflowOptionOrdering( + removeWorkflowOptionTree(step.options, optionKey) + ), })); }; @@ -481,6 +884,7 @@ export function AgentWorkflowSettingsClient({ method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ + agentPresetLibraryConfig: presetLibraryConfig, agentWorkflowUiConfig: config, agentWorkflowMaterialApiConfig: materialApiConfig, agentWorkflowBrandModelConfig: brandModelConfig, @@ -501,6 +905,424 @@ export function AgentWorkflowSettingsClient({ } }; + 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" + /> +
+
+ +