"use client"; import Link from "next/link"; import { useEffect, useMemo, useRef, useState, type ChangeEvent, type ReactNode } from "react"; import useSWR from "swr"; import { toast } from "sonner"; import { motion, AnimatePresence } from "framer-motion"; import { ArrowDown, ArrowLeft, ArrowUp, ChevronDown, ChevronRight, Download, Image as ImageIcon, Layers, Loader2, MonitorPlay, Package, Plus, Save, Search, Settings2, Sparkles, Trash2, Upload, UserCircle, Video, } from "lucide-react"; import { defaultAgentWorkflowConstraintSettings, AGENT_WORKFLOW_MAX_OPTION_DEPTH, AGENT_WORKFLOW_MAX_TREE_DEPTH, countAgentWorkflowOptions, countAgentWorkflowOptionTree, countAgentWorkflowSteps, defaultAgentWorkflowCategoryLabels, defaultAgentWorkflowUiConfig, findAgentWorkflowOptionByKey, findAgentWorkflowStepByKey, flattenAgentWorkflowOptionTree, getEnabledAgentWorkflowOptionTree, getAgentWorkflowCategoryLabels, getAgentWorkflowConstraintSettings, 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 { defaultAgentWorkflowGoodsInfoApiConfig, normalizeAgentWorkflowGoodsInfoApiConfig, } from "@/lib/agents/workflow-goods-info-config"; import { defaultAgentPsdTextColorConfig, normalizeAgentPsdTextColorConfig, resolveAgentPsdTextColorConfigForBrand, } from "@/lib/agents/psd-text-color-config"; import { defaultAgentPsdTemplateSecondaryCategoryConfig, type AgentPsdTemplateSecondaryCategoryConfig, normalizeAgentPsdTemplateSecondaryCategoryConfig, } from "@/lib/agents/psd-template-secondary-category-config"; import { AGENT_WORKFLOW_BRAND_IMPORT_ACCEPT, downloadAgentWorkflowBrandImportTemplate, parseAgentWorkflowBrandImportFile, } from "@/lib/agents/workflow-brand-import"; import { normalizeHexColor } from "@/lib/agents/psd-text-layout"; 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 { SearchableSelect } from "@/components/ui/searchable-select"; import { cn } from "@/lib/utils"; import type { AgentPresetLibraryCategoryNode, AgentPresetLibraryConfig, AgentPsdTextColorBrandConfig, AgentPresetLibrarySection, AgentWorkflowBrandModelConfig, AgentWorkflowBrandStoreEntry, AgentWorkflowCategoryLabels, AgentWorkflowFourthCategoryEntry, AgentWorkflowGoodsInfoApiConfig, AgentWorkflowMaterialApiConfig, AgentWorkflowOptionUiConfig, AgentPsdTextColorConfig, AgentWorkflowStoreEntry, AgentWorkflowThirdCategoryEntry, 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 CHINESE_LEVEL_NUMERALS = ["一", "二", "三", "四", "五", "六", "七", "八"]; const WORKFLOW_LEVEL_LABELS = CHINESE_LEVEL_NUMERALS.map((item) => `${item}级模块`); const WORKFLOW_OPTION_LEVEL_LABELS = CHINESE_LEVEL_NUMERALS.map((item) => `${item}级卡片`); const PRESET_LIBRARY_LEVEL_LABELS = ["一级分类", "二级分类", "三级分类"]; function getWorkflowLevelLabel(depth: number, kind: "模块" | "卡片") { const labels = kind === "模块" ? WORKFLOW_LEVEL_LABELS : WORKFLOW_OPTION_LEVEL_LABELS; return ( labels[Math.max(0, Math.min(depth - 1, labels.length - 1))] || `第 ${depth} 级${kind}` ); } 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}执行。`, blockedOptionKeys: [], children: [], }; } function createWorkflowStepDraft( depth: number, stepIndex: number, existingStepKeys: string[] ): AgentWorkflowStepUiConfig { const levelLabel = getWorkflowLevelLabel(depth, "模块"); 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 collectWorkflowOptionRelations( options: AgentWorkflowOptionUiConfig[], ancestors: string[] = [] ): Array<{ key: string; label: string; depth: number; }> { return options.flatMap((option) => { const currentPath = [...ancestors, option.buttonName || option.key]; return [ { key: option.key, label: currentPath.join(" / "), depth: currentPath.length, }, ...collectWorkflowOptionRelations(option.children || [], currentPath), ]; }); } function collectWorkflowOptionRelationsFromSteps( steps: AgentWorkflowStepUiConfig[] ): Array<{ key: string; label: string; depth: number; stepKey: string; stepTitle: string; }> { return flattenAgentWorkflowSteps(steps).flatMap(({ step }) => collectWorkflowOptionRelations(step.options || []).map((item) => ({ ...item, label: `${step.title} / ${item.label}`, stepKey: step.key, stepTitle: step.title, })) ); } type WorkflowOptionRelation = ReturnType[number]; 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 moveArrayItem(items: T[], fromIndex: number, toIndex: number): T[] { if ( fromIndex === toIndex || fromIndex < 0 || toIndex < 0 || fromIndex >= items.length || toIndex >= items.length ) { return items; } const nextItems = [...items]; const [movedItem] = nextItems.splice(fromIndex, 1); nextItems.splice(toIndex, 0, movedItem); return nextItems; } 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[], priceBands: string[] = [], categoryLabels: Partial = defaultAgentWorkflowCategoryLabels ) { const resolvedCategoryLabels = { ...defaultAgentWorkflowCategoryLabels, ...categoryLabels, }; type FourthCategoryBuilder = { categoryName: string; fifthCategoryMap: Map; sortOrder: number; }; type ThirdCategoryBuilder = { categoryName: string; fourthCategoryMap: Map; sortOrder: number; }; type StoreBuilder = { storeName: string; thirdCategoryMap: Map; sortOrder: number; }; type BrandBuilder = { brandName: string; storeMap: Map; sortOrder: number; }; const brandMap = new Map(); const ensureBrandBuilder = (brandName: string, sortOrder: number) => { const normalizedBrandName = String(brandName || "").trim(); if (!normalizedBrandName) { return null; } const key = normalizedBrandName.toLowerCase(); const existing = brandMap.get(key); if (existing) { return existing; } const nextBuilder: BrandBuilder = { brandName: normalizedBrandName, storeMap: new Map(), sortOrder, }; brandMap.set(key, nextBuilder); return nextBuilder; }; const ensureStoreBuilder = ( brandBuilder: BrandBuilder, storeName: string, sortOrder: number ) => { const normalizedStoreName = String(storeName || "").trim(); if (!normalizedStoreName) { return null; } const key = normalizedStoreName.toLowerCase(); const existing = brandBuilder.storeMap.get(key); if (existing) { return existing; } const nextBuilder: StoreBuilder = { storeName: normalizedStoreName, thirdCategoryMap: new Map(), sortOrder, }; brandBuilder.storeMap.set(key, nextBuilder); return nextBuilder; }; const ensureThirdCategoryBuilder = ( storeBuilder: StoreBuilder, categoryName: string, sortOrder: number ) => { const normalizedCategoryName = String(categoryName || "").trim(); if (!normalizedCategoryName) { return null; } const key = normalizedCategoryName.toLowerCase(); const existing = storeBuilder.thirdCategoryMap.get(key); if (existing) { return existing; } const nextBuilder: ThirdCategoryBuilder = { categoryName: normalizedCategoryName, fourthCategoryMap: new Map(), sortOrder, }; storeBuilder.thirdCategoryMap.set(key, nextBuilder); return nextBuilder; }; const ensureFourthCategoryBuilder = ( thirdCategoryBuilder: ThirdCategoryBuilder, categoryName: string, sortOrder: number ) => { const normalizedCategoryName = String(categoryName || "").trim(); if (!normalizedCategoryName) { return null; } const key = normalizedCategoryName.toLowerCase(); const existing = thirdCategoryBuilder.fourthCategoryMap.get(key); if (existing) { return existing; } const nextBuilder: FourthCategoryBuilder = { categoryName: normalizedCategoryName, fifthCategoryMap: new Map(), sortOrder, }; thirdCategoryBuilder.fourthCategoryMap.set(key, nextBuilder); return nextBuilder; }; entries.forEach((entry, brandIndex) => { const brandBuilder = ensureBrandBuilder(entry.brandName, brandIndex); if (!brandBuilder) { return; } const legacyStoreNames = Array.from( new Set( (entry.stores || []) .map((storeName) => String(storeName || "").trim()) .filter(Boolean) ) ); legacyStoreNames.forEach((storeName, storeIndex) => { ensureStoreBuilder(brandBuilder, storeName, storeIndex); }); (entry.storeEntries || []).forEach((storeEntry, storeIndex) => { const storeBuilder = ensureStoreBuilder(brandBuilder, storeEntry.storeName, storeIndex); if (!storeBuilder) { return; } const legacyThirdCategoryNames = Array.from( new Set( (storeEntry.thirdCategories || []) .map((thirdCategory) => String(thirdCategory || "").trim()) .filter(Boolean) ) ); legacyThirdCategoryNames.forEach((thirdCategory, thirdIndex) => { ensureThirdCategoryBuilder(storeBuilder, thirdCategory, thirdIndex); }); (storeEntry.thirdCategoryEntries || []).forEach((thirdEntry, thirdIndex) => { const thirdCategoryBuilder = ensureThirdCategoryBuilder( storeBuilder, thirdEntry.categoryName, thirdIndex ); if (!thirdCategoryBuilder) { return; } const legacyFourthCategoryNames = Array.from( new Set( (thirdEntry.fourthCategories || []) .map((fourthCategory) => String(fourthCategory || "").trim()) .filter(Boolean) ) ); legacyFourthCategoryNames.forEach((fourthCategory, fourthIndex) => { ensureFourthCategoryBuilder(thirdCategoryBuilder, fourthCategory, fourthIndex); }); (thirdEntry.fourthCategoryEntries || []).forEach((fourthEntry, fourthIndex) => { const fourthCategoryBuilder = ensureFourthCategoryBuilder( thirdCategoryBuilder, fourthEntry.categoryName, fourthIndex ); if (!fourthCategoryBuilder) { return; } (fourthEntry.fifthCategories || []).forEach((fifthCategory) => { const normalizedFifthCategory = String(fifthCategory || "").trim(); if (!normalizedFifthCategory) { return; } const key = normalizedFifthCategory.toLowerCase(); if (!fourthCategoryBuilder.fifthCategoryMap.has(key)) { fourthCategoryBuilder.fifthCategoryMap.set(key, normalizedFifthCategory); } }); }); }); }); }); const normalizedEntries = Array.from(brandMap.values()) .sort((left, right) => left.sortOrder - right.sortOrder) .map((brandEntry, brandIndex) => { const storeEntries = Array.from(brandEntry.storeMap.values()) .sort((left, right) => left.sortOrder - right.sortOrder) .map((storeEntry, storeIndex) => { const thirdCategoryEntries = Array.from(storeEntry.thirdCategoryMap.values()) .sort((left, right) => left.sortOrder - right.sortOrder) .map((thirdEntry, thirdIndex) => { const fourthCategoryEntries = Array.from(thirdEntry.fourthCategoryMap.values()) .sort((left, right) => left.sortOrder - right.sortOrder) .map((fourthEntry, fourthIndex) => ({ categoryName: fourthEntry.categoryName, fifthCategories: Array.from(fourthEntry.fifthCategoryMap.values()), sortOrder: fourthIndex, })); return { categoryName: thirdEntry.categoryName, fourthCategories: fourthCategoryEntries.map((fourthEntry) => fourthEntry.categoryName), fourthCategoryEntries, sortOrder: thirdIndex, }; }); return { storeName: storeEntry.storeName, thirdCategories: thirdCategoryEntries.map((thirdEntry) => thirdEntry.categoryName), thirdCategoryEntries, sortOrder: storeIndex, }; }); return { brandName: brandEntry.brandName, storeEntries, sortOrder: brandIndex, }; }); const normalizedPriceBands = Array.from( new Set( priceBands .map((item) => item.trim()) .filter(Boolean) ) ); return { brandStoreEntries: normalizedEntries.map((entry) => ({ ...entry, stores: entry.storeEntries.map((storeEntry) => storeEntry.storeName), })), brands: normalizedEntries.map((entry) => entry.brandName), stores: Array.from( new Set( normalizedEntries.flatMap((entry) => entry.storeEntries.map((storeEntry) => storeEntry.storeName) ) ) ), thirdCategories: Array.from( new Set( normalizedEntries.flatMap((entry) => entry.storeEntries.flatMap((storeEntry) => storeEntry.thirdCategories || []) ) ) ), fourthCategories: Array.from( new Set( normalizedEntries.flatMap((entry) => entry.storeEntries.flatMap((storeEntry) => (storeEntry.thirdCategoryEntries || []).flatMap( (thirdEntry) => thirdEntry.fourthCategories || [] ) ) ) ) ), fifthCategories: Array.from( new Set( normalizedEntries.flatMap((entry) => entry.storeEntries.flatMap((storeEntry) => (storeEntry.thirdCategoryEntries || []).flatMap((thirdEntry) => (thirdEntry.fourthCategoryEntries || []).flatMap( (fourthEntry) => fourthEntry.fifthCategories || [] ) ) ) ) ) ), categoryLabels: resolvedCategoryLabels, priceBands: normalizedPriceBands, }; } function createStoreDraftKey(brandName: string, storeName: string) { return `${brandName}::${storeName}`; } function createThirdCategoryDraftKey(brandName: string, storeName: string, thirdCategory: string) { return `${brandName}::${storeName}::${thirdCategory}`; } function createFourthCategoryDraftKey( brandName: string, storeName: string, thirdCategory: string, fourthCategory: string ) { return `${brandName}::${storeName}::${thirdCategory}::${fourthCategory}`; } interface AgentWorkflowSettingsClientProps { backHref?: string; backLabel?: string; } export function AgentWorkflowSettingsClient({ backHref = "/settings", backLabel = "返回设置", }: AgentWorkflowSettingsClientProps = {}) { const { data, isLoading, mutate } = useSWR<{ agentPresetLibraryConfig?: AgentPresetLibraryConfig; agentWorkflowUiConfig?: AgentWorkflowUiConfig; agentWorkflowMaterialApiConfig?: AgentWorkflowMaterialApiConfig; agentWorkflowGoodsInfoApiConfig?: AgentWorkflowGoodsInfoApiConfig; agentWorkflowBrandModelConfig?: AgentWorkflowBrandModelConfig; agentWorkflowShowcaseConfig?: AgentWorkflowShowcaseConfig; agentPsdTextColorConfig?: AgentPsdTextColorConfig; agentPsdTemplateSecondaryCategoryConfig?: AgentPsdTemplateSecondaryCategoryConfig; }>("/api/site-settings", fetcher); const [presetLibraryConfig, setPresetLibraryConfig] = useState( defaultAgentPresetLibraryConfig ); const [config, setConfig] = useState(defaultAgentWorkflowUiConfig); const [materialApiConfig, setMaterialApiConfig] = useState( defaultAgentWorkflowMaterialApiConfig ); const [goodsInfoApiConfig, setGoodsInfoApiConfig] = useState( defaultAgentWorkflowGoodsInfoApiConfig ); const [showcaseConfig, setShowcaseConfig] = useState( defaultAgentWorkflowShowcaseConfig ); const [psdTextColorConfig, setPsdTextColorConfig] = useState( defaultAgentPsdTextColorConfig ); const [psdTemplateSecondaryCategoryConfig, setPsdTemplateSecondaryCategoryConfig] = useState( defaultAgentPsdTemplateSecondaryCategoryConfig ); const [brandModelConfig, setBrandModelConfig] = useState( defaultAgentWorkflowBrandModelConfig ); const [isSaving, setIsSaving] = useState(false); const [uploadingKey, setUploadingKey] = useState(null); const [expandedSteps, setExpandedSteps] = useState>(new Set()); const [expandedWorkflowOptions, setExpandedWorkflowOptions] = useState>( {} ); const [expandedPresetLibraryNodes, setExpandedPresetLibraryNodes] = useState< Record >({}); const [activeTab, setActiveTab] = useState("showcase"); const [brandDraft, setBrandDraft] = useState(""); const [priceBandDraft, setPriceBandDraft] = useState(""); const [selectedPsdTextColorBrand, setSelectedPsdTextColorBrand] = useState(""); const [storeDrafts, setStoreDrafts] = useState>({}); const [thirdCategoryDrafts, setThirdCategoryDrafts] = useState>({}); const [fourthCategoryDrafts, setFourthCategoryDrafts] = useState>({}); const [fifthCategoryDrafts, setFifthCategoryDrafts] = useState>({}); const [blockedOptionSearches, setBlockedOptionSearches] = useState>({}); const [isImportingBrandSheet, setIsImportingBrandSheet] = useState(false); const fileInputRefs = useRef>({}); const brandImportInputRef = useRef(null); 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) ); setGoodsInfoApiConfig( normalizeAgentWorkflowGoodsInfoApiConfig(data.agentWorkflowGoodsInfoApiConfig) ); setShowcaseConfig( normalizeAgentWorkflowShowcaseConfig(data.agentWorkflowShowcaseConfig) ); setPsdTextColorConfig( normalizeAgentPsdTextColorConfig(data.agentPsdTextColorConfig) ); setPsdTemplateSecondaryCategoryConfig( normalizeAgentPsdTemplateSecondaryCategoryConfig( data.agentPsdTemplateSecondaryCategoryConfig ) ); setBrandModelConfig( normalizeAgentWorkflowBrandModelConfig(data.agentWorkflowBrandModelConfig) ); setExpandedWorkflowOptions({}); setExpandedPresetLibraryNodes({}); 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 priceBandOptions = useMemo( () => config.inputOptions?.priceBands || defaultAgentWorkflowUiConfig.inputOptions?.priceBands || [], [config.inputOptions?.priceBands] ); const categoryLabels = useMemo( () => getAgentWorkflowCategoryLabels(config), [config] ); const workflowConstraintSettings = useMemo( () => getAgentWorkflowConstraintSettings(config), [config] ); const firstCategoryLabel = categoryLabels.firstCategory; const secondCategoryLabel = categoryLabels.secondCategory; const thirdCategoryLabel = categoryLabels.thirdCategory; const fourthCategoryLabel = categoryLabels.fourthCategory; const fifthCategoryLabel = categoryLabels.fifthCategory; 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 psdTextColorBrandOptions = useMemo( () => Array.from( new Set([ ...configuredBrands, ...(psdTextColorConfig.brandConfigs || []).map((entry) => entry.brandName), ]) ).sort((left, right) => left.localeCompare(right, "zh-CN")), [configuredBrands, psdTextColorConfig.brandConfigs] ); const selectedBrandPsdTextColorConfig = useMemo( () => resolveAgentPsdTextColorConfigForBrand(psdTextColorConfig, selectedPsdTextColorBrand), [psdTextColorConfig, selectedPsdTextColorBrand] ); useEffect(() => { if (psdTextColorBrandOptions.length === 0) { if (selectedPsdTextColorBrand) { setSelectedPsdTextColorBrand(""); } return; } const hasCurrentSelection = psdTextColorBrandOptions.some( (brandName) => brandName === selectedPsdTextColorBrand ); if (!hasCurrentSelection) { setSelectedPsdTextColorBrand(psdTextColorBrandOptions[0] || ""); } }, [psdTextColorBrandOptions, selectedPsdTextColorBrand]); 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 workflowOptionRelations = useMemo( () => collectWorkflowOptionRelationsFromSteps(config.steps || []), [config.steps] ); const workflowOptionRelationLookup = useMemo( () => new Map(workflowOptionRelations.map((item) => [item.key, item] as const)), [workflowOptionRelations] ); const updateInputOptions = (nextEntries: AgentWorkflowBrandStoreEntry[]) => { setConfig((prev) => ({ ...prev, inputOptions: buildWorkflowInputOptionsFromBrandEntries( nextEntries, prev.inputOptions?.priceBands || [], prev.inputOptions?.categoryLabels || categoryLabels ), })); }; const normalizeInputOptionValue = (value: string) => value .replace(/\s+/g, " ") .trim(); const handleCategoryLabelChange = ( key: keyof AgentWorkflowCategoryLabels, value: string ) => { const nextValue = normalizeInputOptionValue(value).slice(0, 24); setConfig((prev) => ({ ...prev, inputOptions: buildWorkflowInputOptionsFromBrandEntries( prev.inputOptions?.brandStoreEntries || [], prev.inputOptions?.priceBands || [], { ...(prev.inputOptions?.categoryLabels || categoryLabels), [key]: nextValue || defaultAgentWorkflowCategoryLabels[key], } ), })); }; const handleAddBrandEntry = () => { const nextValue = normalizeInputOptionValue(brandDraft); if (!nextValue) { toast.error(`请输入${firstCategoryLabel}名称`); return; } if (brandStoreEntries.some((item) => item.brandName.toLowerCase() === nextValue.toLowerCase())) { toast.error(`该${firstCategoryLabel}已存在`); return; } updateInputOptions([ ...brandStoreEntries, { brandName: nextValue, stores: [], storeEntries: [], 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; }); setThirdCategoryDrafts((prev) => { const next = { ...prev }; Object.keys(next).forEach((key) => { if (key.startsWith(`${brandName}::`)) { delete next[key]; } }); return next; }); setFourthCategoryDrafts((prev) => { const next = { ...prev }; Object.keys(next).forEach((key) => { if (key.startsWith(`${brandName}::`)) { delete next[key]; } }); return next; }); setFifthCategoryDrafts((prev) => { const next = { ...prev }; Object.keys(next).forEach((key) => { if (key.startsWith(`${brandName}::`)) { delete next[key]; } }); return next; }); }; const handleAddStoreToBrand = (brandName: string) => { const draft = storeDrafts[brandName] || ""; const nextValue = normalizeInputOptionValue(draft); if (!nextValue) { toast.error(`请输入${secondCategoryLabel}名称`); return; } const targetEntry = brandStoreEntries.find((entry) => entry.brandName === brandName); if ( targetEntry?.storeEntries?.some( (storeEntry) => storeEntry.storeName.toLowerCase() === nextValue.toLowerCase() ) || targetEntry?.stores.some((store) => store.toLowerCase() === nextValue.toLowerCase()) ) { toast.error(`该${secondCategoryLabel}已存在`); return; } updateInputOptions( brandStoreEntries.map((entry) => { if (entry.brandName !== brandName) return entry; return { ...entry, stores: [...entry.stores, nextValue], storeEntries: [ ...(entry.storeEntries || []).map((storeEntry, storeIndex) => ({ ...storeEntry, sortOrder: storeIndex, })), { storeName: nextValue, thirdCategories: [], thirdCategoryEntries: [], sortOrder: (entry.storeEntries || []).length, }, ], }; }) ); 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), storeEntries: (entry.storeEntries || []).filter( (storeEntry) => storeEntry.storeName !== storeName ), } : entry ) ); setThirdCategoryDrafts((prev) => { const next = { ...prev }; Object.keys(next).forEach((key) => { if (key.startsWith(`${brandName}::${storeName}`)) { delete next[key]; } }); return next; }); setFourthCategoryDrafts((prev) => { const next = { ...prev }; Object.keys(next).forEach((key) => { if (key.startsWith(`${brandName}::${storeName}`)) { delete next[key]; } }); return next; }); setFifthCategoryDrafts((prev) => { const next = { ...prev }; Object.keys(next).forEach((key) => { if (key.startsWith(`${brandName}::${storeName}`)) { delete next[key]; } }); return next; }); }; const handleClearBrandTree = () => { if (brandStoreEntries.length === 0) { toast.error("当前没有可清空的层级树数据"); return; } const confirmed = window.confirm( `确认清空当前所有${firstCategoryLabel}/${secondCategoryLabel}及分类层级数据吗?此操作不会影响价格带和其它系统设置。` ); if (!confirmed) { return; } updateInputOptions([]); setBrandDraft(""); setStoreDrafts({}); setThirdCategoryDrafts({}); setFourthCategoryDrafts({}); setFifthCategoryDrafts({}); toast.success("层级树数据已清空,可重新导入表格"); }; const handleAddThirdCategoryToStore = (brandName: string, storeName: string) => { const draftKey = createStoreDraftKey(brandName, storeName); const nextValue = normalizeInputOptionValue(thirdCategoryDrafts[draftKey] || ""); if (!nextValue) { toast.error(`请输入${thirdCategoryLabel}名称`); return; } const targetEntry = brandStoreEntries.find((entry) => entry.brandName === brandName); const targetStoreEntry = targetEntry?.storeEntries?.find( (storeEntry) => storeEntry.storeName === storeName ); if ( (targetStoreEntry?.thirdCategoryEntries || []).some( (thirdEntry) => thirdEntry.categoryName.toLowerCase() === nextValue.toLowerCase() ) || targetStoreEntry?.thirdCategories.some( (thirdCategory) => thirdCategory.toLowerCase() === nextValue.toLowerCase() ) ) { toast.error(`该${thirdCategoryLabel}已存在`); return; } updateInputOptions( brandStoreEntries.map((entry) => { if (entry.brandName !== brandName) return entry; return { ...entry, storeEntries: (entry.storeEntries || []).map((storeEntry) => storeEntry.storeName === storeName ? { ...storeEntry, thirdCategories: [...(storeEntry.thirdCategories || []), nextValue], thirdCategoryEntries: [ ...((storeEntry.thirdCategoryEntries || []).map((thirdEntry, thirdIndex) => ({ ...thirdEntry, sortOrder: thirdIndex, })) || []), { categoryName: nextValue, fourthCategories: [], fourthCategoryEntries: [], sortOrder: (storeEntry.thirdCategoryEntries || []).length, }, ], } : storeEntry ), }; }) ); setThirdCategoryDrafts((prev) => ({ ...prev, [draftKey]: "", })); }; const handleRemoveThirdCategoryFromStore = ( brandName: string, storeName: string, thirdCategory: string ) => { updateInputOptions( brandStoreEntries.map((entry) => { if (entry.brandName !== brandName) return entry; return { ...entry, storeEntries: (entry.storeEntries || []).map((storeEntry) => storeEntry.storeName === storeName ? { ...storeEntry, thirdCategories: (storeEntry.thirdCategories || []).filter( (item) => item !== thirdCategory ), thirdCategoryEntries: (storeEntry.thirdCategoryEntries || []).filter( (item) => item.categoryName !== thirdCategory ), } : storeEntry ), }; }) ); setFourthCategoryDrafts((prev) => { const next = { ...prev }; Object.keys(next).forEach((key) => { if (key.startsWith(`${brandName}::${storeName}::${thirdCategory}`)) { delete next[key]; } }); return next; }); setFifthCategoryDrafts((prev) => { const next = { ...prev }; Object.keys(next).forEach((key) => { if (key.startsWith(`${brandName}::${storeName}::${thirdCategory}`)) { delete next[key]; } }); return next; }); }; const handleAddFourthCategoryToThirdCategory = ( brandName: string, storeName: string, thirdCategory: string ) => { const draftKey = createThirdCategoryDraftKey(brandName, storeName, thirdCategory); const nextValue = normalizeInputOptionValue(fourthCategoryDrafts[draftKey] || ""); if (!nextValue) { toast.error(`请输入${fourthCategoryLabel}名称`); return; } const targetEntry = brandStoreEntries.find((entry) => entry.brandName === brandName); const targetStoreEntry = targetEntry?.storeEntries?.find( (storeEntry) => storeEntry.storeName === storeName ); const targetThirdEntry = targetStoreEntry?.thirdCategoryEntries?.find( (thirdEntry) => thirdEntry.categoryName === thirdCategory ); if ( (targetThirdEntry?.fourthCategoryEntries || []).some( (fourthEntry) => fourthEntry.categoryName.toLowerCase() === nextValue.toLowerCase() ) || (targetThirdEntry?.fourthCategories || []).some( (fourthCategory) => fourthCategory.toLowerCase() === nextValue.toLowerCase() ) ) { toast.error(`该${fourthCategoryLabel}已存在`); return; } updateInputOptions( brandStoreEntries.map((entry) => { if (entry.brandName !== brandName) return entry; return { ...entry, storeEntries: (entry.storeEntries || []).map((storeEntry) => storeEntry.storeName === storeName ? { ...storeEntry, thirdCategoryEntries: (storeEntry.thirdCategoryEntries || []).map((thirdEntry) => thirdEntry.categoryName === thirdCategory ? { ...thirdEntry, fourthCategories: [...(thirdEntry.fourthCategories || []), nextValue], fourthCategoryEntries: [ ...((thirdEntry.fourthCategoryEntries || []).map((fourthEntry, fourthIndex) => ({ ...fourthEntry, sortOrder: fourthIndex, })) || []), { categoryName: nextValue, fifthCategories: [], sortOrder: (thirdEntry.fourthCategoryEntries || []).length, }, ], } : thirdEntry ), } : storeEntry ), }; }) ); setFourthCategoryDrafts((prev) => ({ ...prev, [draftKey]: "", })); }; const handleRemoveFourthCategoryFromThirdCategory = ( brandName: string, storeName: string, thirdCategory: string, fourthCategory: string ) => { updateInputOptions( brandStoreEntries.map((entry) => { if (entry.brandName !== brandName) return entry; return { ...entry, storeEntries: (entry.storeEntries || []).map((storeEntry) => storeEntry.storeName === storeName ? { ...storeEntry, thirdCategoryEntries: (storeEntry.thirdCategoryEntries || []).map((thirdEntry) => thirdEntry.categoryName === thirdCategory ? { ...thirdEntry, fourthCategories: (thirdEntry.fourthCategories || []).filter( (item) => item !== fourthCategory ), fourthCategoryEntries: (thirdEntry.fourthCategoryEntries || []).filter( (item) => item.categoryName !== fourthCategory ), } : thirdEntry ), } : storeEntry ), }; }) ); setFifthCategoryDrafts((prev) => { const next = { ...prev }; Object.keys(next).forEach((key) => { if (key.startsWith(`${brandName}::${storeName}::${thirdCategory}::${fourthCategory}`)) { delete next[key]; } }); return next; }); }; const handleAddFifthCategoryToFourthCategory = ( brandName: string, storeName: string, thirdCategory: string, fourthCategory: string ) => { const draftKey = createFourthCategoryDraftKey( brandName, storeName, thirdCategory, fourthCategory ); const nextValue = normalizeInputOptionValue(fifthCategoryDrafts[draftKey] || ""); if (!nextValue) { toast.error(`请输入${fifthCategoryLabel}名称`); return; } const targetEntry = brandStoreEntries.find((entry) => entry.brandName === brandName); const targetStoreEntry = targetEntry?.storeEntries?.find( (storeEntry) => storeEntry.storeName === storeName ); const targetThirdEntry = targetStoreEntry?.thirdCategoryEntries?.find( (thirdEntry) => thirdEntry.categoryName === thirdCategory ); const targetFourthEntry = targetThirdEntry?.fourthCategoryEntries?.find( (fourthEntry) => fourthEntry.categoryName === fourthCategory ); if ( targetFourthEntry?.fifthCategories.some( (fifthCategory) => fifthCategory.toLowerCase() === nextValue.toLowerCase() ) ) { toast.error(`该${fifthCategoryLabel}已存在`); return; } updateInputOptions( brandStoreEntries.map((entry) => { if (entry.brandName !== brandName) return entry; return { ...entry, storeEntries: (entry.storeEntries || []).map((storeEntry) => storeEntry.storeName === storeName ? { ...storeEntry, thirdCategoryEntries: (storeEntry.thirdCategoryEntries || []).map((thirdEntry) => thirdEntry.categoryName === thirdCategory ? { ...thirdEntry, fourthCategoryEntries: (thirdEntry.fourthCategoryEntries || []).map( (fourthEntry) => fourthEntry.categoryName === fourthCategory ? { ...fourthEntry, fifthCategories: [...(fourthEntry.fifthCategories || []), nextValue], } : fourthEntry ), } : thirdEntry ), } : storeEntry ), }; }) ); setFifthCategoryDrafts((prev) => ({ ...prev, [draftKey]: "", })); }; const handleRemoveFifthCategoryFromFourthCategory = ( brandName: string, storeName: string, thirdCategory: string, fourthCategory: string, fifthCategory: string ) => { updateInputOptions( brandStoreEntries.map((entry) => { if (entry.brandName !== brandName) return entry; return { ...entry, storeEntries: (entry.storeEntries || []).map((storeEntry) => storeEntry.storeName === storeName ? { ...storeEntry, thirdCategoryEntries: (storeEntry.thirdCategoryEntries || []).map((thirdEntry) => thirdEntry.categoryName === thirdCategory ? { ...thirdEntry, fourthCategoryEntries: (thirdEntry.fourthCategoryEntries || []).map( (fourthEntry) => fourthEntry.categoryName === fourthCategory ? { ...fourthEntry, fifthCategories: (fourthEntry.fifthCategories || []).filter( (item) => item !== fifthCategory ), } : fourthEntry ), } : thirdEntry ), } : storeEntry ), }; }) ); }; const handleAddPriceBand = () => { const nextValue = normalizeInputOptionValue(priceBandDraft); if (!nextValue) { toast.error("请输入价格带区间"); return; } if (priceBandOptions.some((item) => item.toLowerCase() === nextValue.toLowerCase())) { toast.error("该价格带已存在"); return; } setConfig((prev) => ({ ...prev, inputOptions: buildWorkflowInputOptionsFromBrandEntries( prev.inputOptions?.brandStoreEntries || [], [...(prev.inputOptions?.priceBands || []), nextValue], prev.inputOptions?.categoryLabels || categoryLabels ), })); setPriceBandDraft(""); }; const handleRemovePriceBand = (priceBand: string) => { setConfig((prev) => ({ ...prev, inputOptions: buildWorkflowInputOptionsFromBrandEntries( prev.inputOptions?.brandStoreEntries || [], (prev.inputOptions?.priceBands || []).filter((item) => item !== priceBand), prev.inputOptions?.categoryLabels || categoryLabels ), })); }; 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 isWorkflowOptionExpanded = (optionKey: string, depth: number) => expandedWorkflowOptions[optionKey] ?? depth === 1; const toggleWorkflowOptionExpansion = (optionKey: string, depth: number) => { setExpandedWorkflowOptions((prev) => ({ ...prev, [optionKey]: !(prev[optionKey] ?? depth === 1), })); }; const isPresetLibraryNodeExpanded = (nodeId: string, depth: number) => expandedPresetLibraryNodes[nodeId] ?? depth === 1; const togglePresetLibraryNodeExpansion = (nodeId: string, depth: number) => { setExpandedPresetLibraryNodes((prev) => ({ ...prev, [nodeId]: !(prev[nodeId] ?? depth === 1), })); }; 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 >= AGENT_WORKFLOW_MAX_OPTION_DEPTH) { 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 handleMoveRootOption = (stepKey: string, optionKey: string, direction: "up" | "down") => { updateStep(stepKey, (step) => { const sortedRootOptions = [...(step.options || [])].sort( (left, right) => left.sortOrder - right.sortOrder ); const currentIndex = sortedRootOptions.findIndex((option) => option.key === optionKey); if (currentIndex < 0) { return step; } const targetIndex = direction === "up" ? currentIndex - 1 : currentIndex + 1; if (targetIndex < 0 || targetIndex >= sortedRootOptions.length) { return step; } return { ...step, options: moveArrayItem(sortedRootOptions, currentIndex, targetIndex).map( (option, index) => ({ ...option, sortOrder: index, }) ), }; }); }; const handleMoveTopLevelStep = (stepKey: string, direction: "up" | "down") => { setConfig((prev) => { const sortedRootSteps = [...prev.steps].sort((left, right) => left.sortOrder - right.sortOrder); const currentIndex = sortedRootSteps.findIndex((step) => step.key === stepKey); if (currentIndex < 0) { return prev; } const targetIndex = direction === "up" ? currentIndex - 1 : currentIndex + 1; if (targetIndex < 0 || targetIndex >= sortedRootSteps.length) { return prev; } return normalizeConfigOrdering({ ...prev, steps: moveArrayItem(sortedRootSteps, currentIndex, targetIndex).map((step, index) => ({ ...step, sortOrder: index, })), }); }); }; 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 updateSelectedBrandPsdTextColorConfig = ( updater: (current: AgentPsdTextColorBrandConfig | AgentPsdTextColorConfig) => { enabled: boolean; colors: [string, string]; } ) => { if (!selectedPsdTextColorBrand) { setPsdTextColorConfig((prev) => { const next = updater(prev); return { ...prev, enabled: next.enabled, colors: next.colors, }; }); return; } setPsdTextColorConfig((prev) => { const nextBrandConfigs = [...(prev.brandConfigs || [])]; const matchedIndex = nextBrandConfigs.findIndex( (entry) => entry.brandName.trim().toLowerCase() === selectedPsdTextColorBrand.trim().toLowerCase() ); const currentBrandConfig = matchedIndex >= 0 ? nextBrandConfigs[matchedIndex] : resolveAgentPsdTextColorConfigForBrand(prev, selectedPsdTextColorBrand); const next = updater(currentBrandConfig); const nextEntry: AgentPsdTextColorBrandConfig = { brandName: selectedPsdTextColorBrand, enabled: next.enabled, colors: next.colors, }; if (matchedIndex >= 0) { nextBrandConfigs[matchedIndex] = nextEntry; } else { nextBrandConfigs.push(nextEntry); } return { ...prev, brandConfigs: nextBrandConfigs.sort((left, right) => left.brandName.localeCompare(right.brandName, "zh-CN") ), }; }); }; const updatePsdTextColor = (index: 0 | 1, value: string) => { const normalized = normalizeHexColor( value, selectedBrandPsdTextColorConfig.colors[index] || "#111111" ); updateSelectedBrandPsdTextColorConfig((current) => { const nextColors = [...current.colors] as [string, string]; nextColors[index] = normalized; return { enabled: current.enabled, colors: nextColors, }; }); }; 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, agentWorkflowGoodsInfoApiConfig: goodsInfoApiConfig, agentWorkflowBrandModelConfig: brandModelConfig, agentWorkflowShowcaseConfig: showcaseConfig, agentPsdTextColorConfig: psdTextColorConfig, agentPsdTemplateSecondaryCategoryConfig: psdTemplateSecondaryCategoryConfig, }), }); if (!response.ok) { let message = "保存失败"; try { const payload = await response.json(); if (Array.isArray(payload?.details) && payload.details.length > 0) { const firstIssue = payload.details[0] as { message?: string; path?: Array; }; const pathText = Array.isArray(firstIssue.path) && firstIssue.path.length > 0 ? firstIssue.path .filter((segment) => segment !== undefined && segment !== null && segment !== "") .join(" / ") : ""; const reasonText = String(firstIssue.message || "").trim(); message = [payload?.error, pathText, reasonText].filter(Boolean).join(":"); } else if (typeof payload?.error === "string" && payload.error.trim()) { message = payload.error.trim(); } } catch { // Ignore JSON parsing issues and keep the generic message. } throw new Error(message); } toast.success("智能体工作流配置已保存"); await mutate(); } catch (error) { toast.error(error instanceof Error ? error.message : "保存失败"); } finally { setIsSaving(false); } }; const handleDownloadBrandTemplate = async () => { try { await downloadAgentWorkflowBrandImportTemplate(categoryLabels); toast.success("层级导入模板已开始下载"); } catch (error) { toast.error(error instanceof Error ? error.message : "下载模板失败"); } }; const handleBrandSheetImport = async (event: ChangeEvent) => { const file = event.target.files?.[0]; event.target.value = ""; if (!file) { return; } try { setIsImportingBrandSheet(true); const { entries, rowCount } = await parseAgentWorkflowBrandImportFile(file, categoryLabels); updateInputOptions([...brandStoreEntries, ...entries]); toast.success(`已从表格导入 ${rowCount} 行,层级树已自动合并去重`); } catch (error) { toast.error(error instanceof Error ? error.message : "导入表格失败"); } finally { setIsImportingBrandSheet(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 ); const isExpanded = isPresetLibraryNodeExpanded(node.id, depth); return (
1 && "pl-4 sm:pl-6")}> togglePresetLibraryNodeExpansion(node.id, depth)} > 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 = getWorkflowLevelLabel(depth, "卡片"); const sortedChildren = [...(option.children || [])].sort( (left, right) => left.sortOrder - right.sortOrder ); const canAddChild = depth < AGENT_WORKFLOW_MAX_OPTION_DEPTH; const blockedCandidates = workflowOptionRelations.filter( (item) => item.key !== option.key && (workflowConstraintSettings.crossLevelMutualExclusionEnabled || item.depth === depth) ); const selectedBlockedItems = (option.blockedOptionKeys || []) .map((blockedKey) => workflowOptionRelationLookup.get(blockedKey)) .filter((item): item is WorkflowOptionRelation => Boolean(item)); const isExpanded = isWorkflowOptionExpanded(option.key, depth); const blockedSearchValue = blockedOptionSearches[refKey] || ""; const normalizedBlockedSearchValue = blockedSearchValue.trim().toLowerCase(); const filteredBlockedCandidates = blockedCandidates.filter((candidate) => { if (!normalizedBlockedSearchValue) { return true; } return [candidate.label, candidate.stepTitle, candidate.key].some((value) => value.toLowerCase().includes(normalizedBlockedSearchValue) ); }); const canMoveUp = depth === 1 && indexPath[0] > 1; const canMoveDown = depth === 1 && indexPath[0] < (step.options || []).length; return (
1 && "pl-4 sm:pl-6")}> toggleWorkflowOptionExpansion(option.key, depth)} >
1 && "border-l-4 border-l-sky-500/30" )} >
{indexPath.join(".")} {levelLabel} {sortedChildren.length} 子卡片 {!option.enabled ? 前台显示开发中 : null} {option.buttonName || "未命名卡片"} {option.key}
{depth > 1 ? (
{isExpanded ? "已展开当前子卡片配置" : "当前子卡片已折叠,点击左侧箭头展开详细设置"}
) : null}
{depth === 1 ? ( <> ) : null} {canAddChild ? ( ) : ( {`已达 ${AGENT_WORKFLOW_MAX_OPTION_DEPTH} 级`} )}
{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" />