From ea6a9a9d76122c83247692c9c5f2897e857877ce Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 2 Apr 2026 13:59:33 +0800 Subject: [PATCH] feat: extend workflow brand hierarchy to five levels --- .../agent-workflow-settings-client.tsx | 677 +++++++++++++++++- app/api/site-settings/route.ts | 28 + app/canvas/[id]/page.tsx | 16 + components/agents/AgentPageDark.tsx | 24 +- components/agents/AgentWorkflowPanel.tsx | 80 ++- components/agents/AgentWorkflowPanelDark.tsx | 72 +- .../agents/AgentWorkflowPanelDarkV2.tsx | 74 +- components/agents/AgentWorkflowPanelNew.tsx | 72 +- lib/agents/generation-project-state.ts | 4 + lib/agents/workflow-composer.ts | 16 + lib/agents/workflow-ui-config.ts | 277 ++++++- lib/db/schema.ts | 2 + types/agent.ts | 20 + 13 files changed, 1298 insertions(+), 64 deletions(-) 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 a06a0cb30..f3e36cd51 100644 --- a/app/(admin)/settings/agent-workflow/agent-workflow-settings-client.tsx +++ b/app/(admin)/settings/agent-workflow/agent-workflow-settings-client.tsx @@ -92,11 +92,13 @@ import type { AgentPresetLibrarySection, AgentWorkflowBrandModelConfig, AgentWorkflowBrandStoreEntry, + AgentWorkflowFourthCategoryEntry, AgentWorkflowGoodsInfoApiConfig, AgentWorkflowMaterialApiConfig, AgentWorkflowOptionUiConfig, AgentPsdTextColorConfig, AgentWorkflowStoreEntry, + AgentWorkflowThirdCategoryEntry, AgentWorkflowShowcaseConfig, AgentWorkflowStepUiConfig, AgentWorkflowUiConfig, @@ -465,15 +467,86 @@ function buildWorkflowInputOptionsFromBrandEntries( return; } + const uniqueThirdCategoryEntries = new Map(); + + (storeEntry.thirdCategoryEntries || []).forEach((thirdEntry, thirdIndex) => { + const categoryName = String(thirdEntry.categoryName || "").trim(); + if (!categoryName) { + return; + } + + const uniqueFourthCategoryEntries = new Map(); + (thirdEntry.fourthCategoryEntries || []).forEach((fourthEntry, fourthIndex) => { + const fourthCategoryName = String(fourthEntry.categoryName || "").trim(); + if (!fourthCategoryName) { + return; + } + + uniqueFourthCategoryEntries.set(fourthCategoryName.toLowerCase(), { + categoryName: fourthCategoryName, + fifthCategories: Array.from( + new Set( + (fourthEntry.fifthCategories || []) + .map((fifthCategory) => fifthCategory.trim()) + .filter(Boolean) + ) + ), + sortOrder: fourthIndex, + }); + }); + + const fourthCategoryEntries = Array.from(uniqueFourthCategoryEntries.values()).map( + (fourthEntry, fourthIndex) => ({ + ...fourthEntry, + sortOrder: fourthIndex, + }) + ); + + uniqueThirdCategoryEntries.set(categoryName.toLowerCase(), { + categoryName, + fourthCategories: + fourthCategoryEntries.length > 0 + ? fourthCategoryEntries.map((fourthEntry) => fourthEntry.categoryName) + : Array.from( + new Set( + (thirdEntry.fourthCategories || []) + .map((fourthCategory) => fourthCategory.trim()) + .filter(Boolean) + ) + ), + fourthCategoryEntries, + sortOrder: thirdIndex, + }); + }); + + const thirdCategoryEntries = Array.from(uniqueThirdCategoryEntries.values()).map((thirdEntry, thirdIndex) => ({ + ...thirdEntry, + fourthCategories: + (thirdEntry.fourthCategoryEntries || []).length > 0 + ? (thirdEntry.fourthCategoryEntries || []).map((fourthEntry) => fourthEntry.categoryName) + : Array.from( + new Set( + (thirdEntry.fourthCategories || []) + .map((fourthCategory) => fourthCategory.trim()) + .filter(Boolean) + ) + ), + sortOrder: thirdIndex, + })); + uniqueStoreEntries.set(storeName.toLowerCase(), { storeName, - thirdCategories: Array.from( - new Set( - (storeEntry.thirdCategories || []) - .map((thirdCategory) => thirdCategory.trim()) - .filter(Boolean) - ) - ), + thirdCategories: + thirdCategoryEntries.length > 0 + ? thirdCategoryEntries.map((thirdEntry) => thirdEntry.categoryName) + : Array.from( + new Set( + (storeEntry.thirdCategories || []) + .map((thirdCategory) => thirdCategory.trim()) + .filter(Boolean) + ) + ), + thirdCategoryEntries, sortOrder: storeIndex, }); }); @@ -513,10 +586,51 @@ function buildWorkflowInputOptionsFromBrandEntries( ) ) ), + 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 || [] + ) + ) + ) + ) + ) + ), 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; @@ -573,6 +687,8 @@ export function AgentWorkflowSettingsClient({ const [priceBandDraft, setPriceBandDraft] = useState(""); const [storeDrafts, setStoreDrafts] = useState>({}); const [thirdCategoryDrafts, setThirdCategoryDrafts] = useState>({}); + const [fourthCategoryDrafts, setFourthCategoryDrafts] = useState>({}); + const [fifthCategoryDrafts, setFifthCategoryDrafts] = useState>({}); const [blockedOptionSearches, setBlockedOptionSearches] = useState>({}); const fileInputRefs = useRef>({}); @@ -717,6 +833,24 @@ export function AgentWorkflowSettingsClient({ }); 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) => { @@ -752,6 +886,7 @@ export function AgentWorkflowSettingsClient({ { storeName: nextValue, thirdCategories: [], + thirdCategoryEntries: [], sortOrder: (entry.storeEntries || []).length, }, ], @@ -780,13 +915,35 @@ export function AgentWorkflowSettingsClient({ ); setThirdCategoryDrafts((prev) => { const next = { ...prev }; - delete next[`${brandName}::${storeName}`]; + 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 handleAddThirdCategoryToStore = (brandName: string, storeName: string) => { - const draftKey = `${brandName}::${storeName}`; + const draftKey = createStoreDraftKey(brandName, storeName); const nextValue = normalizeInputOptionValue(thirdCategoryDrafts[draftKey] || ""); if (!nextValue) { toast.error("请输入三级分类名称"); @@ -798,6 +955,9 @@ export function AgentWorkflowSettingsClient({ (storeEntry) => storeEntry.storeName === storeName ); if ( + (targetStoreEntry?.thirdCategoryEntries || []).some( + (thirdEntry) => thirdEntry.categoryName.toLowerCase() === nextValue.toLowerCase() + ) || targetStoreEntry?.thirdCategories.some( (thirdCategory) => thirdCategory.toLowerCase() === nextValue.toLowerCase() ) @@ -816,6 +976,18 @@ export function AgentWorkflowSettingsClient({ ? { ...storeEntry, thirdCategories: [...(storeEntry.thirdCategories || []), nextValue], + thirdCategoryEntries: [ + ...((storeEntry.thirdCategoryEntries || []).map((thirdEntry, thirdIndex) => ({ + ...thirdEntry, + sortOrder: thirdIndex, + })) || []), + { + categoryName: nextValue, + fourthCategories: [], + fourthCategoryEntries: [], + sortOrder: (storeEntry.thirdCategoryEntries || []).length, + }, + ], } : storeEntry ), @@ -845,6 +1017,259 @@ export function AgentWorkflowSettingsClient({ 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("请输入四级分类名称"); + 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("该四级分类已存在"); + 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("请输入五级分类名称"); + 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("该五级分类已存在"); + 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 ), @@ -2726,7 +3151,7 @@ export function AgentWorkflowSettingsClient({
基础下拉选项 - 配置品牌、店铺、三级分类的层级关系,以及第一页可选的价格带区间 + 配置品牌、店铺到五级分类的层级关系,以及第一页可选的价格带区间
@@ -2841,7 +3266,7 @@ export function AgentWorkflowSettingsClient({ - {(storeEntry.thirdCategories || []).length} 个三级分类 + {(storeEntry.thirdCategoryEntries || []).length} 个三级分类 @@ -2883,32 +3308,214 @@ export function AgentWorkflowSettingsClient({ - {(storeEntry.thirdCategories || []).length > 0 ? ( -
- {(storeEntry.thirdCategories || []).map( - (thirdCategory) => ( + {(storeEntry.thirdCategoryEntries || []).length > 0 ? ( +
+ {(storeEntry.thirdCategoryEntries || []).map((thirdEntry) => { + const fourthDraftKey = createThirdCategoryDraftKey( + entry.brandName, + storeEntry.storeName, + thirdEntry.categoryName + ); + return (
- {thirdCategory} - +
+
+
+ {thirdEntry.categoryName} + +
+ + {(thirdEntry.fourthCategoryEntries || []).length} 个四级分类 + +
+
+ +
+
+ + setFourthCategoryDrafts((prev) => ({ + ...prev, + [fourthDraftKey]: e.target.value, + })) + } + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + handleAddFourthCategoryToThirdCategory( + entry.brandName, + storeEntry.storeName, + thirdEntry.categoryName + ); + } + }} + placeholder={`给三级分类“${thirdEntry.categoryName}”添加四级分类`} + /> + +
+ + {(thirdEntry.fourthCategoryEntries || []).length > 0 ? ( +
+ {(thirdEntry.fourthCategoryEntries || []).map( + (fourthEntry) => { + const fifthDraftKey = createFourthCategoryDraftKey( + entry.brandName, + storeEntry.storeName, + thirdEntry.categoryName, + fourthEntry.categoryName + ); + return ( +
+
+
+
+ {fourthEntry.categoryName} + +
+ + {(fourthEntry.fifthCategories || []).length} 个五级分类 + +
+
+ +
+
+ + setFifthCategoryDrafts((prev) => ({ + ...prev, + [fifthDraftKey]: e.target.value, + })) + } + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + handleAddFifthCategoryToFourthCategory( + entry.brandName, + storeEntry.storeName, + thirdEntry.categoryName, + fourthEntry.categoryName + ); + } + }} + placeholder={`给四级分类“${fourthEntry.categoryName}”添加五级分类`} + /> + +
+ + {(fourthEntry.fifthCategories || []).length > 0 ? ( +
+ {(fourthEntry.fifthCategories || []).map( + (fifthCategory) => ( +
+ {fifthCategory} + +
+ ) + )} +
+ ) : ( +
+ 这个四级分类下还没有添加任何五级分类。 +
+ )} +
+
+ ); + } + )} +
+ ) : ( +
+ 这个三级分类下还没有添加任何四级分类。 +
+ )} +
- ) - )} + ); + })}
) : (
@@ -2931,7 +3538,7 @@ export function AgentWorkflowSettingsClient({
) : (
- 还没有添加品牌。先添加品牌,再在每个品牌下继续添加店铺和三级分类。 + 还没有添加品牌。先添加品牌,再在每个品牌下继续添加店铺以及三级到五级分类。
)}
diff --git a/app/api/site-settings/route.ts b/app/api/site-settings/route.ts index b5c1224bd..b6308e5db 100644 --- a/app/api/site-settings/route.ts +++ b/app/api/site-settings/route.ts @@ -731,6 +731,8 @@ const updateSchema = z.object({ brands: z.array(z.string().trim().min(1).max(64)).max(300).optional(), stores: z.array(z.string().trim().min(1).max(64)).max(300).optional(), thirdCategories: z.array(z.string().trim().min(1).max(64)).max(300).optional(), + fourthCategories: z.array(z.string().trim().min(1).max(64)).max(300).optional(), + fifthCategories: z.array(z.string().trim().min(1).max(64)).max(300).optional(), priceBands: z.array(z.string().trim().min(1).max(64)).max(300).optional(), brandStoreEntries: z .array( @@ -745,6 +747,32 @@ const updateSchema = z.object({ .array(z.string().trim().min(1).max(64)) .max(300) .optional(), + thirdCategoryEntries: z + .array( + z.object({ + categoryName: z.string().trim().min(1).max(64), + fourthCategories: z + .array(z.string().trim().min(1).max(64)) + .max(300) + .optional(), + fourthCategoryEntries: z + .array( + z.object({ + categoryName: z.string().trim().min(1).max(64), + fifthCategories: z + .array(z.string().trim().min(1).max(64)) + .max(300) + .optional(), + sortOrder: z.number().min(0).max(999).optional(), + }) + ) + .max(300) + .optional(), + sortOrder: z.number().min(0).max(999).optional(), + }) + ) + .max(300) + .optional(), sortOrder: z.number().min(0).max(999).optional(), }) ) diff --git a/app/canvas/[id]/page.tsx b/app/canvas/[id]/page.tsx index 7dc63701a..c6e95a8db 100644 --- a/app/canvas/[id]/page.tsx +++ b/app/canvas/[id]/page.tsx @@ -153,6 +153,8 @@ function hasMeaningfulWorkflowState( String(inputs?.brand || "").trim() || String(inputs?.store || "").trim() || String(inputs?.thirdCategory || "").trim() || + String(inputs?.fourthCategory || "").trim() || + String(inputs?.fifthCategory || "").trim() || String(inputs?.priceBand || "").trim() ); @@ -322,6 +324,8 @@ export default function CanvasEditorPage({ params }: PageProps) { brand: "", store: "", thirdCategory: "", + fourthCategory: "", + fifthCategory: "", priceBand: "", }); const [workflowMaterials, setWorkflowMaterials] = useState( @@ -489,6 +493,8 @@ export default function CanvasEditorPage({ params }: PageProps) { brand: workflowInputs.brand.trim(), store: workflowInputs.store.trim(), thirdCategory: workflowInputs.thirdCategory.trim(), + fourthCategory: workflowInputs.fourthCategory.trim(), + fifthCategory: workflowInputs.fifthCategory.trim(), priceBand: workflowInputs.priceBand.trim(), }, materials: workflowMaterials, @@ -535,6 +541,8 @@ export default function CanvasEditorPage({ params }: PageProps) { brand: "", store: "", thirdCategory: "", + fourthCategory: "", + fifthCategory: "", priceBand: "", }); setWorkflowMaterials(createEmptyAgentWorkflowMaterialSelections()); @@ -567,6 +575,8 @@ export default function CanvasEditorPage({ params }: PageProps) { brand: String(persistedWorkflow.inputs?.brand || "").trim(), store: String(persistedWorkflow.inputs?.store || "").trim(), thirdCategory: String(persistedWorkflow.inputs?.thirdCategory || "").trim(), + fourthCategory: String(persistedWorkflow.inputs?.fourthCategory || "").trim(), + fifthCategory: String(persistedWorkflow.inputs?.fifthCategory || "").trim(), priceBand: String(persistedWorkflow.inputs?.priceBand || "").trim(), }); setWorkflowMaterials( @@ -676,6 +686,8 @@ export default function CanvasEditorPage({ params }: PageProps) { brand: workflowInputs.brand.trim(), store: workflowInputs.store.trim(), thirdCategory: workflowInputs.thirdCategory.trim(), + fourthCategory: workflowInputs.fourthCategory.trim(), + fifthCategory: workflowInputs.fifthCategory.trim(), priceBand: workflowInputs.priceBand.trim(), }, materials: workflowMaterials, @@ -763,6 +775,8 @@ export default function CanvasEditorPage({ params }: PageProps) { brand: String(parsed.inputs?.brand || "").trim(), store: String(parsed.inputs?.store || "").trim(), thirdCategory: String(parsed.inputs?.thirdCategory || "").trim(), + fourthCategory: String(parsed.inputs?.fourthCategory || "").trim(), + fifthCategory: String(parsed.inputs?.fifthCategory || "").trim(), priceBand: String(parsed.inputs?.priceBand || "").trim(), }); setWorkflowMaterials(normalizeAgentWorkflowMaterialSelections(parsed.materials)); @@ -890,6 +904,8 @@ export default function CanvasEditorPage({ params }: PageProps) { brand: String(persistedWorkflow.inputs?.brand || "").trim(), store: String(persistedWorkflow.inputs?.store || "").trim(), thirdCategory: String(persistedWorkflow.inputs?.thirdCategory || "").trim(), + fourthCategory: String(persistedWorkflow.inputs?.fourthCategory || "").trim(), + fifthCategory: String(persistedWorkflow.inputs?.fifthCategory || "").trim(), priceBand: String(persistedWorkflow.inputs?.priceBand || "").trim(), }); setWorkflowMaterials(normalizeAgentWorkflowMaterialSelections(persistedWorkflow.materials)); diff --git a/components/agents/AgentPageDark.tsx b/components/agents/AgentPageDark.tsx index 30241448f..157a43719 100644 --- a/components/agents/AgentPageDark.tsx +++ b/components/agents/AgentPageDark.tsx @@ -9,10 +9,10 @@ import { createEmptyAgentWorkflowMaterialSelections, normalizeAgentWorkflowMaterialSelections, } from "@/lib/agents/workflow-material-config"; +import { buildAgentWorkflowComposerContext } from "@/lib/agents/workflow-composer"; import type { AgentWorkflowMaterialSelections, AgentWorkflowStepKey, - AgentWorkflowComposerContext, } from "@/types/agent"; interface AgentPageDarkProps { @@ -38,6 +38,8 @@ export function AgentPageDark({ brand: "", store: "", thirdCategory: "", + fourthCategory: "", + fifthCategory: "", priceBand: "", }); const [workflowMaterials, setWorkflowMaterials] = useState( @@ -56,24 +58,12 @@ export function AgentPageDark({ }; // Build workflow context - const workflowContext: AgentWorkflowComposerContext | null = { + const workflowContext = buildAgentWorkflowComposerContext({ + config: workflowConfig, inputs: workflowInputs, materials: workflowMaterials, - selections: Object.entries(workflowSelections).map(([stepKey, optionKey]) => { - const step = workflowConfig?.steps?.find((s: any) => s.key === stepKey); - const option = step?.options?.find((o: any) => o.key === optionKey); - return { - stepKey, - stepTitle: step?.title || "", - optionKey: optionKey || "", - optionName: option?.buttonName || "", - promptFragment: option?.promptFragment || "", - }; - }), - referenceImages: [], - summaryText: "", - promptText: "", - }; + selections: workflowSelections, + }); return (
diff --git a/components/agents/AgentWorkflowPanel.tsx b/components/agents/AgentWorkflowPanel.tsx index 44162e76b..5ab639283 100644 --- a/components/agents/AgentWorkflowPanel.tsx +++ b/components/agents/AgentWorkflowPanel.tsx @@ -11,6 +11,8 @@ import { cn } from "@/lib/utils"; import { defaultAgentWorkflowUiConfig, getAgentWorkflowBrandStoreEntries, + getAgentWorkflowFifthCategoryOptionsByBrandStoreThirdFourth, + getAgentWorkflowFourthCategoryOptionsByBrandStoreThirdCategory, getAgentWorkflowPriceBandOptions, getAgentWorkflowThirdCategoryOptionsByBrandStore, getAgentWorkflowStoreOptionsByBrand, @@ -33,6 +35,8 @@ export interface AgentWorkflowInputs { brand: string; store: string; thirdCategory: string; + fourthCategory: string; + fifthCategory: string; priceBand: string; } @@ -109,6 +113,27 @@ export function AgentWorkflowPanel({ }), [config, inputs.brand, inputs.store] ); + const fourthCategoryOptions = useMemo( + () => + getAgentWorkflowFourthCategoryOptionsByBrandStoreThirdCategory({ + config, + brand: inputs.brand, + store: inputs.store, + thirdCategory: inputs.thirdCategory, + }), + [config, inputs.brand, inputs.store, inputs.thirdCategory] + ); + const fifthCategoryOptions = useMemo( + () => + getAgentWorkflowFifthCategoryOptionsByBrandStoreThirdFourth({ + config, + brand: inputs.brand, + store: inputs.store, + thirdCategory: inputs.thirdCategory, + fourthCategory: inputs.fourthCategory, + }), + [config, inputs.brand, inputs.store, inputs.thirdCategory, inputs.fourthCategory] + ); const priceBandOptions = useMemo( () => getAgentWorkflowPriceBandOptions(config), [config] @@ -198,6 +223,8 @@ export function AgentWorkflowPanel({ brand: "", store: "", thirdCategory: "", + fourthCategory: "", + fifthCategory: "", priceBand: "", }); onMaterialsChange({ @@ -217,7 +244,7 @@ export function AgentWorkflowPanel({
基础配置
-
+
@@ -254,6 +283,8 @@ export function AgentWorkflowPanel({ value.trim().toLowerCase() === inputs.store.trim().toLowerCase() ? inputs.thirdCategory : "", + fourthCategory: "", + fifthCategory: "", }) } /> @@ -264,7 +295,44 @@ export function AgentWorkflowPanel({ searchPlaceholder="输入三级分类筛选" emptyText={inputs.store ? "当前店铺下暂无三级分类" : "请先选择店铺"} disabled={!inputs.store || thirdCategoryOptions.length === 0} - onValueChange={(value) => onInputsChange({ ...inputs, thirdCategory: value })} + onValueChange={(value) => + onInputsChange({ + ...inputs, + thirdCategory: value, + fourthCategory: + value.trim().toLowerCase() === inputs.thirdCategory.trim().toLowerCase() + ? inputs.fourthCategory + : "", + fifthCategory: "", + }) + } + /> + + onInputsChange({ + ...inputs, + fourthCategory: value, + fifthCategory: + value.trim().toLowerCase() === inputs.fourthCategory.trim().toLowerCase() + ? inputs.fifthCategory + : "", + }) + } + /> + onInputsChange({ ...inputs, fifthCategory: value })} /> 0 || materials.secondary.selectedImageIds.length > 0 ? ( @@ -376,6 +446,12 @@ export function AgentWorkflowPanel({ {inputs.thirdCategory ? ( 三级分类:{inputs.thirdCategory} ) : null} + {inputs.fourthCategory ? ( + 四级分类:{inputs.fourthCategory} + ) : null} + {inputs.fifthCategory ? ( + 五级分类:{inputs.fifthCategory} + ) : null} {inputs.priceBand ? ( 价格带:{inputs.priceBand} ) : null} diff --git a/components/agents/AgentWorkflowPanelDark.tsx b/components/agents/AgentWorkflowPanelDark.tsx index bd73ced0b..850ccc390 100644 --- a/components/agents/AgentWorkflowPanelDark.tsx +++ b/components/agents/AgentWorkflowPanelDark.tsx @@ -12,6 +12,8 @@ import { cn } from "@/lib/utils"; import { defaultAgentWorkflowUiConfig, getAgentWorkflowBrandStoreEntries, + getAgentWorkflowFifthCategoryOptionsByBrandStoreThirdFourth, + getAgentWorkflowFourthCategoryOptionsByBrandStoreThirdCategory, getAgentWorkflowPriceBandOptions, getAgentWorkflowThirdCategoryOptionsByBrandStore, getAgentWorkflowStoreOptionsByBrand, @@ -34,6 +36,8 @@ export interface AgentWorkflowInputs { brand: string; store: string; thirdCategory: string; + fourthCategory: string; + fifthCategory: string; priceBand: string; } @@ -191,6 +195,27 @@ export function AgentWorkflowPanelDark({ }), [config, inputs.brand, inputs.store] ); + const fourthCategoryOptions = useMemo( + () => + getAgentWorkflowFourthCategoryOptionsByBrandStoreThirdCategory({ + config, + brand: inputs.brand, + store: inputs.store, + thirdCategory: inputs.thirdCategory, + }), + [config, inputs.brand, inputs.store, inputs.thirdCategory] + ); + const fifthCategoryOptions = useMemo( + () => + getAgentWorkflowFifthCategoryOptionsByBrandStoreThirdFourth({ + config, + brand: inputs.brand, + store: inputs.store, + thirdCategory: inputs.thirdCategory, + fourthCategory: inputs.fourthCategory, + }), + [config, inputs.brand, inputs.store, inputs.thirdCategory, inputs.fourthCategory] + ); const priceBandOptions = useMemo( () => getAgentWorkflowPriceBandOptions(config), [config] @@ -284,7 +309,7 @@ export function AgentWorkflowPanelDark({ onReset(); return; } - onInputsChange({ primarySku: "", secondarySku: "", brand: "", store: "", thirdCategory: "", priceBand: "" }); + onInputsChange({ primarySku: "", secondarySku: "", brand: "", store: "", thirdCategory: "", fourthCategory: "", fifthCategory: "", priceBand: "" }); onMaterialsChange({ primary: createEmptyAgentWorkflowMaterialSelection(), secondary: createEmptyAgentWorkflowMaterialSelection(), @@ -318,7 +343,7 @@ export function AgentWorkflowPanelDark({ 基础配置 @@ -359,6 +386,8 @@ export function AgentWorkflowPanelDark({ value.trim().toLowerCase() === inputs.store.trim().toLowerCase() ? inputs.thirdCategory : "", + fourthCategory: "", + fifthCategory: "", }) } /> @@ -369,7 +398,44 @@ export function AgentWorkflowPanelDark({ searchPlaceholder="输入三级分类筛选" emptyText={inputs.store ? "当前店铺下暂无三级分类" : "请先选择店铺"} disabled={!inputs.store || thirdCategoryOptions.length === 0} - onValueChange={(value) => onInputsChange({ ...inputs, thirdCategory: value })} + onValueChange={(value) => + onInputsChange({ + ...inputs, + thirdCategory: value, + fourthCategory: + value.trim().toLowerCase() === inputs.thirdCategory.trim().toLowerCase() + ? inputs.fourthCategory + : "", + fifthCategory: "", + }) + } + /> + + onInputsChange({ + ...inputs, + fourthCategory: value, + fifthCategory: + value.trim().toLowerCase() === inputs.fourthCategory.trim().toLowerCase() + ? inputs.fifthCategory + : "", + }) + } + /> + onInputsChange({ ...inputs, fifthCategory: value })} /> config?.inputOptions?.brands || defaultAgentWorkflowUiConfig.inputOptions?.brands || [], @@ -297,6 +301,27 @@ export function AgentWorkflowBrandStorePanel({ }), [config, inputs.brand, inputs.store] ); + const fourthCategoryOptions = useMemo( + () => + getAgentWorkflowFourthCategoryOptionsByBrandStoreThirdCategory({ + config, + brand: inputs.brand, + store: inputs.store, + thirdCategory: inputs.thirdCategory, + }), + [config, inputs.brand, inputs.store, inputs.thirdCategory] + ); + const fifthCategoryOptions = useMemo( + () => + getAgentWorkflowFifthCategoryOptionsByBrandStoreThirdFourth({ + config, + brand: inputs.brand, + store: inputs.store, + thirdCategory: inputs.thirdCategory, + fourthCategory: inputs.fourthCategory, + }), + [config, inputs.brand, inputs.store, inputs.thirdCategory, inputs.fourthCategory] + ); const priceBandOptions = useMemo( () => getAgentWorkflowPriceBandOptions(config), [config] @@ -318,7 +343,7 @@ export function AgentWorkflowBrandStorePanel({ {description}
-
+
@@ -357,6 +384,8 @@ export function AgentWorkflowBrandStorePanel({ value.trim().toLowerCase() === inputs.store.trim().toLowerCase() ? inputs.thirdCategory : "", + fourthCategory: "", + fifthCategory: "", }) } /> @@ -368,7 +397,46 @@ export function AgentWorkflowBrandStorePanel({ emptyText={inputs.store ? "当前店铺下暂无三级分类" : "请先选择店铺"} theme={theme} disabled={!inputs.store || thirdCategoryOptions.length === 0} - onValueChange={(value) => onInputsChange({ ...inputs, thirdCategory: value })} + onValueChange={(value) => + onInputsChange({ + ...inputs, + thirdCategory: value, + fourthCategory: + value.trim().toLowerCase() === inputs.thirdCategory.trim().toLowerCase() + ? inputs.fourthCategory + : "", + fifthCategory: "", + }) + } + /> + + onInputsChange({ + ...inputs, + fourthCategory: value, + fifthCategory: + value.trim().toLowerCase() === inputs.fourthCategory.trim().toLowerCase() + ? inputs.fifthCategory + : "", + }) + } + /> + onInputsChange({ ...inputs, fifthCategory: value })} /> + getAgentWorkflowFourthCategoryOptionsByBrandStoreThirdCategory({ + config, + brand: inputs.brand, + store: inputs.store, + thirdCategory: inputs.thirdCategory, + }), + [config, inputs.brand, inputs.store, inputs.thirdCategory] + ); + const fifthCategoryOptions = useMemo( + () => + getAgentWorkflowFifthCategoryOptionsByBrandStoreThirdFourth({ + config, + brand: inputs.brand, + store: inputs.store, + thirdCategory: inputs.thirdCategory, + fourthCategory: inputs.fourthCategory, + }), + [config, inputs.brand, inputs.store, inputs.thirdCategory, inputs.fourthCategory] + ); const priceBandOptions = useMemo( () => getAgentWorkflowPriceBandOptions(config), [config] @@ -305,7 +330,7 @@ export function AgentWorkflowPanelNew({ onReset(); return; } - onInputsChange({ primarySku: "", secondarySku: "", brand: "", store: "", thirdCategory: "", priceBand: "" }); + onInputsChange({ primarySku: "", secondarySku: "", brand: "", store: "", thirdCategory: "", fourthCategory: "", fifthCategory: "", priceBand: "" }); onMaterialsChange({ primary: createEmptyAgentWorkflowMaterialSelection(), secondary: createEmptyAgentWorkflowMaterialSelection(), @@ -357,7 +382,7 @@ export function AgentWorkflowPanelNew({ 基础配置 -
+
@@ -394,6 +421,8 @@ export function AgentWorkflowPanelNew({ value.trim().toLowerCase() === inputs.store.trim().toLowerCase() ? inputs.thirdCategory : "", + fourthCategory: "", + fifthCategory: "", }) } /> @@ -404,7 +433,44 @@ export function AgentWorkflowPanelNew({ searchPlaceholder="输入三级分类筛选" emptyText={inputs.store ? "当前店铺下暂无三级分类" : "请先选择店铺"} disabled={!inputs.store || thirdCategoryOptions.length === 0} - onValueChange={(value) => onInputsChange({ ...inputs, thirdCategory: value })} + onValueChange={(value) => + onInputsChange({ + ...inputs, + thirdCategory: value, + fourthCategory: + value.trim().toLowerCase() === inputs.thirdCategory.trim().toLowerCase() + ? inputs.fourthCategory + : "", + fifthCategory: "", + }) + } + /> + + onInputsChange({ + ...inputs, + fourthCategory: value, + fifthCategory: + value.trim().toLowerCase() === inputs.fourthCategory.trim().toLowerCase() + ? inputs.fifthCategory + : "", + }) + } + /> + onInputsChange({ ...inputs, fifthCategory: value })} /> ).brand || "" : "").trim(), store: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record).store || "" : "").trim(), thirdCategory: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record).thirdCategory || "" : "").trim(), + fourthCategory: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record).fourthCategory || "" : "").trim(), + fifthCategory: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record).fifthCategory || "" : "").trim(), priceBand: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record).priceBand || "" : "").trim(), }, materials: normalizeAgentWorkflowMaterialSelections(raw.materials), diff --git a/lib/agents/workflow-composer.ts b/lib/agents/workflow-composer.ts index e5d2c2659..aa9d79f9d 100644 --- a/lib/agents/workflow-composer.ts +++ b/lib/agents/workflow-composer.ts @@ -21,6 +21,8 @@ export interface AgentWorkflowInputsValue { brand: string; store: string; thirdCategory: string; + fourthCategory: string; + fifthCategory: string; priceBand: string; } @@ -216,6 +218,12 @@ export function buildAgentWorkflowComposerContext(params: { if (params.inputs.thirdCategory.trim()) { summaryParts.push(`三级分类:${params.inputs.thirdCategory.trim()}`); } + if (params.inputs.fourthCategory.trim()) { + summaryParts.push(`四级分类:${params.inputs.fourthCategory.trim()}`); + } + if (params.inputs.fifthCategory.trim()) { + summaryParts.push(`五级分类:${params.inputs.fifthCategory.trim()}`); + } if (params.inputs.priceBand.trim()) { summaryParts.push(`价格带:${params.inputs.priceBand.trim()}`); } @@ -276,6 +284,12 @@ export function buildAgentWorkflowComposerContext(params: { if (params.inputs.thirdCategory.trim()) { promptLines.push(`三级分类:${params.inputs.thirdCategory.trim()}`); } + if (params.inputs.fourthCategory.trim()) { + promptLines.push(`四级分类:${params.inputs.fourthCategory.trim()}`); + } + if (params.inputs.fifthCategory.trim()) { + promptLines.push(`五级分类:${params.inputs.fifthCategory.trim()}`); + } if (params.inputs.priceBand.trim()) { promptLines.push(`价格带:${params.inputs.priceBand.trim()}`); } @@ -362,6 +376,8 @@ export function buildAgentWorkflowComposerContext(params: { brand: params.inputs.brand.trim() || undefined, store: params.inputs.store.trim() || undefined, thirdCategory: params.inputs.thirdCategory.trim() || undefined, + fourthCategory: params.inputs.fourthCategory.trim() || undefined, + fifthCategory: params.inputs.fifthCategory.trim() || undefined, priceBand: params.inputs.priceBand.trim() || undefined, selections, referenceImages, diff --git a/lib/agents/workflow-ui-config.ts b/lib/agents/workflow-ui-config.ts index b881484bb..b99a10569 100644 --- a/lib/agents/workflow-ui-config.ts +++ b/lib/agents/workflow-ui-config.ts @@ -1,7 +1,9 @@ import type { AgentWorkflowBrandStoreEntry, + AgentWorkflowFourthCategoryEntry, AgentWorkflowOptionUiConfig, AgentWorkflowStoreEntry, + AgentWorkflowThirdCategoryEntry, AgentWorkflowStepKey, AgentWorkflowStepUiConfig, AgentWorkflowUiConfig, @@ -141,6 +143,8 @@ export const defaultAgentWorkflowUiConfig: AgentWorkflowUiConfig = { brands: [], stores: [], thirdCategories: [], + fourthCategories: [], + fifthCategories: [], priceBands: [], brandStoreEntries: [], }, @@ -434,6 +438,143 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC const normalizeBrandStoreEntries = (input: unknown) => { if (!Array.isArray(input)) return [] as AgentWorkflowBrandStoreEntry[]; + const normalizeFourthCategoryEntries = ( + rawFourthCategoryEntries: unknown, + legacyFourthCategories: string[] + ): AgentWorkflowFourthCategoryEntry[] => { + const mappedEntries: Array = Array.isArray( + rawFourthCategoryEntries + ) + ? rawFourthCategoryEntries.map((fourthItem, fourthIndex) => { + if (typeof fourthItem === "string") { + const categoryName = normalizeText(fourthItem, 64); + if (!categoryName) return null; + return { + categoryName, + fifthCategories: [], + sortOrder: fourthIndex, + }; + } + + const fourthRecord = + fourthItem && typeof fourthItem === "object" && !Array.isArray(fourthItem) + ? (fourthItem as Record) + : {}; + const categoryName = normalizeText( + fourthRecord.categoryName ?? fourthRecord.fourthCategoryName, + 64 + ); + if (!categoryName) return null; + return { + categoryName, + fifthCategories: normalizeStringList(fourthRecord.fifthCategories).slice(0, 300), + sortOrder: normalizeNumber(fourthRecord.sortOrder, fourthIndex), + }; + }) + : []; + + const normalizedFromEntries = mappedEntries + .filter((item): item is AgentWorkflowFourthCategoryEntry => item !== null) + .sort((left, right) => (left.sortOrder || 0) - (right.sortOrder || 0)) + .map((entry, index) => ({ + ...entry, + fifthCategories: Array.from( + new Set((entry.fifthCategories || []).map((item) => item.trim()).filter(Boolean)) + ).slice(0, 300), + sortOrder: index, + })); + + if (normalizedFromEntries.length > 0) { + return normalizedFromEntries; + } + + return legacyFourthCategories.map((categoryName, index) => ({ + categoryName, + fifthCategories: [], + sortOrder: index, + })); + }; + + const normalizeThirdCategoryEntries = ( + rawThirdCategoryEntries: unknown, + legacyThirdCategories: string[] + ): AgentWorkflowThirdCategoryEntry[] => { + const mappedEntries: Array = Array.isArray( + rawThirdCategoryEntries + ) + ? rawThirdCategoryEntries.map((thirdItem, thirdIndex) => { + if (typeof thirdItem === "string") { + const categoryName = normalizeText(thirdItem, 64); + if (!categoryName) return null; + return { + categoryName, + fourthCategories: [], + fourthCategoryEntries: [], + sortOrder: thirdIndex, + }; + } + + const thirdRecord = + thirdItem && typeof thirdItem === "object" && !Array.isArray(thirdItem) + ? (thirdItem as Record) + : {}; + const categoryName = normalizeText( + thirdRecord.categoryName ?? thirdRecord.thirdCategoryName, + 64 + ); + if (!categoryName) return null; + + const legacyFourthCategories = normalizeStringList(thirdRecord.fourthCategories).slice( + 0, + 300 + ); + const fourthCategoryEntries = normalizeFourthCategoryEntries( + thirdRecord.fourthCategoryEntries, + legacyFourthCategories + ); + + return { + categoryName, + fourthCategories: + fourthCategoryEntries.length > 0 + ? fourthCategoryEntries.map((entry) => entry.categoryName) + : legacyFourthCategories, + fourthCategoryEntries, + sortOrder: normalizeNumber(thirdRecord.sortOrder, thirdIndex), + }; + }) + : []; + + const normalizedFromEntries = mappedEntries + .filter((item): item is AgentWorkflowThirdCategoryEntry => item !== null) + .sort((left, right) => (left.sortOrder || 0) - (right.sortOrder || 0)) + .map((entry, index) => ({ + ...entry, + fourthCategories: Array.from( + new Set((entry.fourthCategories || []).map((item) => item.trim()).filter(Boolean)) + ).slice(0, 300), + fourthCategoryEntries: (entry.fourthCategoryEntries || []).map((fourthEntry, fourthIndex) => ({ + ...fourthEntry, + fifthCategories: Array.from( + new Set((fourthEntry.fifthCategories || []).map((item) => item.trim()).filter(Boolean)) + ).slice(0, 300), + sortOrder: fourthIndex, + })), + sortOrder: index, + })); + + if (normalizedFromEntries.length > 0) { + return normalizedFromEntries; + } + + return legacyThirdCategories.map((categoryName, index) => ({ + categoryName, + fourthCategories: [], + fourthCategoryEntries: [], + sortOrder: index, + })); + }; + const normalizeStoreEntries = ( rawStoreEntries: unknown, legacyStores: string[] @@ -456,9 +597,21 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC : {}; const storeName = normalizeText(storeRecord.storeName, 64); if (!storeName) return null; + const legacyThirdCategories = normalizeStringList(storeRecord.thirdCategories).slice( + 0, + 300 + ); + const thirdCategoryEntries = normalizeThirdCategoryEntries( + storeRecord.thirdCategoryEntries, + legacyThirdCategories + ); return { storeName, - thirdCategories: normalizeStringList(storeRecord.thirdCategories).slice(0, 300), + thirdCategories: + thirdCategoryEntries.length > 0 + ? thirdCategoryEntries.map((entry) => entry.categoryName) + : legacyThirdCategories, + thirdCategoryEntries, sortOrder: normalizeNumber(storeRecord.sortOrder, storeIndex), }; }) @@ -472,6 +625,22 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC thirdCategories: Array.from( new Set((entry.thirdCategories || []).map((item) => item.trim()).filter(Boolean)) ).slice(0, 300), + thirdCategoryEntries: (entry.thirdCategoryEntries || []).map((thirdEntry, thirdIndex) => ({ + ...thirdEntry, + fourthCategories: Array.from( + new Set((thirdEntry.fourthCategories || []).map((item) => item.trim()).filter(Boolean)) + ).slice(0, 300), + fourthCategoryEntries: (thirdEntry.fourthCategoryEntries || []).map( + (fourthEntry, fourthIndex) => ({ + ...fourthEntry, + fifthCategories: Array.from( + new Set((fourthEntry.fifthCategories || []).map((item) => item.trim()).filter(Boolean)) + ).slice(0, 300), + sortOrder: fourthIndex, + }) + ), + sortOrder: thirdIndex, + })), sortOrder: index, })); @@ -482,6 +651,7 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC return legacyStores.map((storeName, index) => ({ storeName, thirdCategories: [], + thirdCategoryEntries: [], sortOrder: index, })); }; @@ -519,6 +689,8 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC const normalizedBrands = normalizeStringList(rawInputOptions.brands); const normalizedStores = normalizeStringList(rawInputOptions.stores); const normalizedThirdCategories = normalizeStringList(rawInputOptions.thirdCategories); + const normalizedFourthCategories = normalizeStringList(rawInputOptions.fourthCategories); + const normalizedFifthCategories = normalizeStringList(rawInputOptions.fifthCategories); const normalizedPriceBands = normalizeStringList(rawInputOptions.priceBands); const fallbackBrandStoreEntries: AgentWorkflowBrandStoreEntry[] = normalizedBrandStoreEntries.length > 0 @@ -531,6 +703,7 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC ? normalizedStores.map((storeName, storeIndex) => ({ storeName, thirdCategories: [], + thirdCategoryEntries: [], sortOrder: storeIndex, })) : [], @@ -718,6 +891,36 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC ) ) : normalizedThirdCategories, + fourthCategories: + fallbackBrandStoreEntries.length > 0 + ? Array.from( + new Set( + fallbackBrandStoreEntries.flatMap((entry) => + (entry.storeEntries || []).flatMap((storeEntry) => + (storeEntry.thirdCategoryEntries || []).flatMap( + (thirdEntry) => thirdEntry.fourthCategories || [] + ) + ) + ) + ) + ) + : normalizedFourthCategories, + fifthCategories: + fallbackBrandStoreEntries.length > 0 + ? Array.from( + new Set( + fallbackBrandStoreEntries.flatMap((entry) => + (entry.storeEntries || []).flatMap((storeEntry) => + (storeEntry.thirdCategoryEntries || []).flatMap((thirdEntry) => + (thirdEntry.fourthCategoryEntries || []).flatMap( + (fourthEntry) => fourthEntry.fifthCategories || [] + ) + ) + ) + ) + ) + ) + : normalizedFifthCategories, priceBands: normalizedPriceBands, brandStoreEntries: fallbackBrandStoreEntries, }, @@ -773,6 +976,13 @@ export function getAgentWorkflowThirdCategoryOptionsByBrandStore(params: { const matchedStoreEntry = matchedEntry?.storeEntries?.find( (storeEntry) => storeEntry.storeName.trim().toLowerCase() === normalizedStore ); + const thirdCategoryOptions = + matchedStoreEntry?.thirdCategoryEntries && matchedStoreEntry.thirdCategoryEntries.length > 0 + ? matchedStoreEntry.thirdCategoryEntries.map((entry) => entry.categoryName).filter(Boolean) + : matchedStoreEntry?.thirdCategories || []; + if (thirdCategoryOptions.length > 0) { + return thirdCategoryOptions; + } if (matchedStoreEntry && matchedStoreEntry.thirdCategories.length > 0) { return matchedStoreEntry.thirdCategories; } @@ -780,6 +990,71 @@ export function getAgentWorkflowThirdCategoryOptionsByBrandStore(params: { return params.config?.inputOptions?.thirdCategories || defaultAgentWorkflowUiConfig.inputOptions?.thirdCategories || []; } +export function getAgentWorkflowFourthCategoryOptionsByBrandStoreThirdCategory(params: { + config?: AgentWorkflowUiConfig | null; + brand?: string | null; + store?: string | null; + thirdCategory?: string | null; +}) { + const entries = getAgentWorkflowBrandStoreEntries(params.config); + const normalizedBrand = String(params.brand || "").trim().toLowerCase(); + const normalizedStore = String(params.store || "").trim().toLowerCase(); + const normalizedThirdCategory = String(params.thirdCategory || "").trim().toLowerCase(); + if (!normalizedBrand || !normalizedStore || !normalizedThirdCategory) { + return []; + } + + const matchedEntry = entries.find( + (entry) => entry.brandName.trim().toLowerCase() === normalizedBrand + ); + const matchedStoreEntry = matchedEntry?.storeEntries?.find( + (storeEntry) => storeEntry.storeName.trim().toLowerCase() === normalizedStore + ); + const matchedThirdEntry = matchedStoreEntry?.thirdCategoryEntries?.find( + (thirdEntry) => thirdEntry.categoryName.trim().toLowerCase() === normalizedThirdCategory + ); + if (matchedThirdEntry && matchedThirdEntry.fourthCategories.length > 0) { + return matchedThirdEntry.fourthCategories; + } + + return params.config?.inputOptions?.fourthCategories || defaultAgentWorkflowUiConfig.inputOptions?.fourthCategories || []; +} + +export function getAgentWorkflowFifthCategoryOptionsByBrandStoreThirdFourth(params: { + config?: AgentWorkflowUiConfig | null; + brand?: string | null; + store?: string | null; + thirdCategory?: string | null; + fourthCategory?: string | null; +}) { + const entries = getAgentWorkflowBrandStoreEntries(params.config); + const normalizedBrand = String(params.brand || "").trim().toLowerCase(); + const normalizedStore = String(params.store || "").trim().toLowerCase(); + const normalizedThirdCategory = String(params.thirdCategory || "").trim().toLowerCase(); + const normalizedFourthCategory = String(params.fourthCategory || "").trim().toLowerCase(); + if (!normalizedBrand || !normalizedStore || !normalizedThirdCategory || !normalizedFourthCategory) { + return []; + } + + const matchedEntry = entries.find( + (entry) => entry.brandName.trim().toLowerCase() === normalizedBrand + ); + const matchedStoreEntry = matchedEntry?.storeEntries?.find( + (storeEntry) => storeEntry.storeName.trim().toLowerCase() === normalizedStore + ); + const matchedThirdEntry = matchedStoreEntry?.thirdCategoryEntries?.find( + (thirdEntry) => thirdEntry.categoryName.trim().toLowerCase() === normalizedThirdCategory + ); + const matchedFourthEntry = matchedThirdEntry?.fourthCategoryEntries?.find( + (fourthEntry) => fourthEntry.categoryName.trim().toLowerCase() === normalizedFourthCategory + ); + if (matchedFourthEntry && matchedFourthEntry.fifthCategories.length > 0) { + return matchedFourthEntry.fifthCategories; + } + + return params.config?.inputOptions?.fifthCategories || defaultAgentWorkflowUiConfig.inputOptions?.fifthCategories || []; +} + export function getAgentWorkflowPriceBandOptions(config?: AgentWorkflowUiConfig | null) { return config?.inputOptions?.priceBands || defaultAgentWorkflowUiConfig.inputOptions?.priceBands || []; } diff --git a/lib/db/schema.ts b/lib/db/schema.ts index 8f4efe438..96b12fbc7 100644 --- a/lib/db/schema.ts +++ b/lib/db/schema.ts @@ -618,6 +618,8 @@ export const canvasProject = pgTable("CanvasProject", { brand?: string; store?: string; thirdCategory?: string; + fourthCategory?: string; + fifthCategory?: string; priceBand?: string; }; materials?: { diff --git a/types/agent.ts b/types/agent.ts index 89058cab5..451b5b8c6 100644 --- a/types/agent.ts +++ b/types/agent.ts @@ -106,15 +106,31 @@ export interface AgentWorkflowUiConfig { brands: string[]; stores: string[]; thirdCategories?: string[]; + fourthCategories?: string[]; + fifthCategories?: string[]; priceBands?: string[]; brandStoreEntries?: AgentWorkflowBrandStoreEntry[]; }; steps: AgentWorkflowStepUiConfig[]; } +export interface AgentWorkflowFourthCategoryEntry { + categoryName: string; + fifthCategories: string[]; + sortOrder?: number; +} + +export interface AgentWorkflowThirdCategoryEntry { + categoryName: string; + fourthCategories: string[]; + fourthCategoryEntries?: AgentWorkflowFourthCategoryEntry[]; + sortOrder?: number; +} + export interface AgentWorkflowStoreEntry { storeName: string; thirdCategories: string[]; + thirdCategoryEntries?: AgentWorkflowThirdCategoryEntry[]; sortOrder?: number; } @@ -258,6 +274,8 @@ export interface AgentWorkflowComposerContext { brand?: string; store?: string; thirdCategory?: string; + fourthCategory?: string; + fifthCategory?: string; priceBand?: string; selections: AgentWorkflowComposerSelection[]; referenceImages: AgentWorkflowReferenceImage[]; @@ -392,6 +410,8 @@ export interface AgentWorkflowPersistedState { brand: string; store: string; thirdCategory: string; + fourthCategory: string; + fifthCategory: string; priceBand: string; }; materials?: AgentWorkflowMaterialSelections;