feat: extend workflow category and card controls
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-04-02 12:35:24 +08:00
parent f0086e26da
commit 7eda4720d3
13 changed files with 731 additions and 68 deletions

View File

@ -6,10 +6,11 @@ import useSWR from "swr";
import { toast } from "sonner";
import { motion, AnimatePresence } from "framer-motion";
import {
ArrowDown,
ArrowLeft,
ArrowUp,
ChevronDown,
ChevronRight,
GripVertical,
Image as ImageIcon,
Layers,
Loader2,
@ -95,6 +96,7 @@ import type {
AgentWorkflowMaterialApiConfig,
AgentWorkflowOptionUiConfig,
AgentPsdTextColorConfig,
AgentWorkflowStoreEntry,
AgentWorkflowShowcaseConfig,
AgentWorkflowStepUiConfig,
AgentWorkflowUiConfig,
@ -306,6 +308,23 @@ function removeWorkflowStepTree(
}));
}
function moveArrayItem<T>(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);
}
@ -437,13 +456,33 @@ function buildWorkflowInputOptionsFromBrandEntries(
) {
const normalizedEntries = entries.map((entry, index) => ({
brandName: entry.brandName.trim(),
stores: Array.from(
new Set(
(entry.stores || [])
.map((store) => store.trim())
.filter(Boolean)
)
),
storeEntries: (() => {
const uniqueStoreEntries = new Map<string, AgentWorkflowStoreEntry>();
(entry.storeEntries || []).forEach((storeEntry, storeIndex) => {
const storeName = String(storeEntry.storeName || "").trim();
if (!storeName) {
return;
}
uniqueStoreEntries.set(storeName.toLowerCase(), {
storeName,
thirdCategories: Array.from(
new Set(
(storeEntry.thirdCategories || [])
.map((thirdCategory) => thirdCategory.trim())
.filter(Boolean)
)
),
sortOrder: storeIndex,
});
});
return Array.from(uniqueStoreEntries.values()).map((storeEntry, storeIndex) => ({
...storeEntry,
sortOrder: storeIndex,
}));
})(),
sortOrder: index,
}));
const normalizedPriceBands = Array.from(
@ -455,9 +494,25 @@ function buildWorkflowInputOptionsFromBrandEntries(
);
return {
brandStoreEntries: normalizedEntries,
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.stores))),
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 || [])
)
)
),
priceBands: normalizedPriceBands,
};
}
@ -517,6 +572,8 @@ export function AgentWorkflowSettingsClient({
const [brandDraft, setBrandDraft] = useState("");
const [priceBandDraft, setPriceBandDraft] = useState("");
const [storeDrafts, setStoreDrafts] = useState<Record<string, string>>({});
const [thirdCategoryDrafts, setThirdCategoryDrafts] = useState<Record<string, string>>({});
const [blockedOptionSearches, setBlockedOptionSearches] = useState<Record<string, string>>({});
const fileInputRefs = useRef<Record<string, HTMLInputElement | null>>({});
useEffect(() => {
@ -597,6 +654,14 @@ export function AgentWorkflowSettingsClient({
[...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,
@ -629,6 +694,7 @@ export function AgentWorkflowSettingsClient({
{
brandName: nextValue,
stores: [],
storeEntries: [],
sortOrder: brandStoreEntries.length,
},
]);
@ -642,6 +708,15 @@ export function AgentWorkflowSettingsClient({
delete next[brandName];
return next;
});
setThirdCategoryDrafts((prev) => {
const next = { ...prev };
Object.keys(next).forEach((key) => {
if (key.startsWith(`${brandName}::`)) {
delete next[key];
}
});
return next;
});
};
const handleAddStoreToBrand = (brandName: string) => {
@ -653,7 +728,12 @@ export function AgentWorkflowSettingsClient({
}
const targetEntry = brandStoreEntries.find((entry) => entry.brandName === brandName);
if (targetEntry?.stores.some((store) => store.toLowerCase() === nextValue.toLowerCase())) {
if (
targetEntry?.storeEntries?.some(
(storeEntry) => storeEntry.storeName.toLowerCase() === nextValue.toLowerCase()
) ||
targetEntry?.stores.some((store) => store.toLowerCase() === nextValue.toLowerCase())
) {
toast.error("该店铺已存在");
return;
}
@ -664,6 +744,17 @@ export function AgentWorkflowSettingsClient({
return {
...entry,
stores: [...entry.stores, nextValue],
storeEntries: [
...(entry.storeEntries || []).map((storeEntry, storeIndex) => ({
...storeEntry,
sortOrder: storeIndex,
})),
{
storeName: nextValue,
thirdCategories: [],
sortOrder: (entry.storeEntries || []).length,
},
],
};
})
);
@ -680,10 +771,86 @@ export function AgentWorkflowSettingsClient({
? {
...entry,
stores: entry.stores.filter((store) => store !== storeName),
storeEntries: (entry.storeEntries || []).filter(
(storeEntry) => storeEntry.storeName !== storeName
),
}
: entry
)
);
setThirdCategoryDrafts((prev) => {
const next = { ...prev };
delete next[`${brandName}::${storeName}`];
return next;
});
};
const handleAddThirdCategoryToStore = (brandName: string, storeName: string) => {
const draftKey = `${brandName}::${storeName}`;
const nextValue = normalizeInputOptionValue(thirdCategoryDrafts[draftKey] || "");
if (!nextValue) {
toast.error("请输入三级分类名称");
return;
}
const targetEntry = brandStoreEntries.find((entry) => entry.brandName === brandName);
const targetStoreEntry = targetEntry?.storeEntries?.find(
(storeEntry) => storeEntry.storeName === storeName
);
if (
targetStoreEntry?.thirdCategories.some(
(thirdCategory) => thirdCategory.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,
thirdCategories: [...(storeEntry.thirdCategories || []), nextValue],
}
: 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
),
}
: storeEntry
),
};
})
);
};
const handleAddPriceBand = () => {
@ -1020,6 +1187,33 @@ export function AgentWorkflowSettingsClient({
}));
};
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 handleUploadPreview = async (stepKey: string, optionKey: string, file: File) => {
if (!file.type.startsWith("image/")) {
toast.error("请上传图片文件");
@ -1346,13 +1540,26 @@ export function AgentWorkflowSettingsClient({
(left, right) => left.sortOrder - right.sortOrder
);
const canAddChild = depth < AGENT_WORKFLOW_MAX_OPTION_DEPTH;
const optionRelations = collectWorkflowOptionRelationsFromSteps(config.steps || []);
const optionLookup = new Map(optionRelations.map((item) => [item.key, item] as const));
const blockedCandidates = optionRelations.filter((item) => item.key !== option.key);
const blockedCandidates = workflowOptionRelations.filter(
(item) => item.key !== option.key && item.depth === depth
);
const selectedBlockedItems = (option.blockedOptionKeys || [])
.map((blockedKey) => optionLookup.get(blockedKey))
.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 (
<div key={option.key} className={cn("space-y-4", depth > 1 && "pl-4 sm:pl-6")}>
@ -1403,6 +1610,30 @@ export function AgentWorkflowSettingsClient({
</div>
<div className="flex flex-wrap items-center gap-1">
{depth === 1 ? (
<>
<Button
type="button"
variant="outline"
size="icon"
className="h-7 w-7"
disabled={!canMoveUp}
onClick={() => handleMoveRootOption(step.key, option.key, "up")}
>
<ArrowUp className="h-3.5 w-3.5" />
</Button>
<Button
type="button"
variant="outline"
size="icon"
className="h-7 w-7"
disabled={!canMoveDown}
onClick={() => handleMoveRootOption(step.key, option.key, "down")}
>
<ArrowDown className="h-3.5 w-3.5" />
</Button>
</>
) : null}
{canAddChild ? (
<Button
type="button"
@ -1535,7 +1766,7 @@ export function AgentWorkflowSettingsClient({
<div className="space-y-1">
<Label className="text-xs">互斥卡片限制</Label>
<p className="text-[11px] leading-5 text-muted-foreground">
选中当前卡片后,下方指定的其它卡片会在前台变灰并不可选。可跨所有流程节点,并覆盖各节点下的所有层级子卡片。
当前只展示同级卡片供你配置互斥。一级只与所有一级卡片互斥,二级只与所有二级卡片互斥;生效时会自动覆盖目标卡片下的全部子级。
</p>
</div>
@ -1574,9 +1805,24 @@ export function AgentWorkflowSettingsClient({
</div>
)}
<div className="max-h-44 space-y-2 overflow-y-auto rounded-md border bg-background/70 p-2">
{blockedCandidates.length > 0 ? (
blockedCandidates.map((candidate) => {
<div className="space-y-2 rounded-md border bg-background/70 p-2">
<div className="relative">
<Search className="pointer-events-none absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
<Input
value={blockedSearchValue}
onChange={(e) =>
setBlockedOptionSearches((prev) => ({
...prev,
[refKey]: e.target.value,
}))
}
placeholder={`搜索同级卡片(${blockedCandidates.length} 个候选)`}
className="h-8 pl-8 text-xs"
/>
</div>
<div className="max-h-44 space-y-2 overflow-y-auto">
{filteredBlockedCandidates.length > 0 ? (
filteredBlockedCandidates.map((candidate) => {
const active = (option.blockedOptionKeys || []).includes(candidate.key);
return (
<button
@ -1610,7 +1856,7 @@ export function AgentWorkflowSettingsClient({
{candidate.label}
</span>
<span className="block text-[10px] text-muted-foreground">
{getWorkflowLevelLabel(candidate.depth, "卡片")}
{getWorkflowLevelLabel(candidate.depth, "卡片")} · {candidate.key}
</span>
</span>
<Badge variant={active ? "default" : "outline"}>
@ -1618,12 +1864,15 @@ export function AgentWorkflowSettingsClient({
</Badge>
</button>
);
})
) : (
<div className="px-2 py-3 text-center text-xs text-muted-foreground">
当前工作流下没有其他可限制卡片
</div>
)}
})
) : (
<div className="px-2 py-3 text-center text-xs text-muted-foreground">
{blockedCandidates.length > 0
? "没有命中搜索结果,换个关键词试试"
: "当前工作流下没有其他可限制的同级卡片"}
</div>
)}
</div>
</div>
</div>
</div>
@ -2477,7 +2726,7 @@ export function AgentWorkflowSettingsClient({
</div>
<div>
<CardTitle className="text-base">基础下拉选项</CardTitle>
<CardDescription>配置品牌、店铺母子关系,以及第一页可选的价格带区间</CardDescription>
<CardDescription>配置品牌、店铺、三级分类的层级关系,以及第一页可选的价格带区间</CardDescription>
</div>
</div>
</CardHeader>
@ -2530,7 +2779,7 @@ export function AgentWorkflowSettingsClient({
</button>
</div>
<span className="text-xs text-muted-foreground">
{entry.stores.length} 个店铺
{(entry.storeEntries || []).length} 个店铺
</span>
</div>
</div>
@ -2564,24 +2813,112 @@ export function AgentWorkflowSettingsClient({
</Button>
</div>
{entry.stores.length > 0 ? (
<div className="mt-3 flex flex-wrap gap-2">
{entry.stores.map((store) => (
<div
key={`${entry.brandName}-${store}`}
className="inline-flex items-center gap-2 rounded-full border bg-background px-3 py-1.5 text-sm"
>
<span>{store}</span>
<button
type="button"
onClick={() => handleRemoveStoreFromBrand(entry.brandName, store)}
className="rounded-full p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-destructive"
aria-label={`删除店铺 ${store}`}
{(entry.storeEntries || []).length > 0 ? (
<div className="mt-3 space-y-3">
{(entry.storeEntries || []).map((storeEntry) => {
const draftKey = `${entry.brandName}::${storeEntry.storeName}`;
return (
<div
key={`${entry.brandName}-${storeEntry.storeName}`}
className="rounded-xl border bg-background/80 p-3"
>
<Trash2 className="h-3.5 w-3.5" />
</button>
</div>
))}
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-2">
<div className="inline-flex items-center gap-2 rounded-full border bg-background px-3 py-1.5 text-sm font-medium">
<span>{storeEntry.storeName}</span>
<button
type="button"
onClick={() =>
handleRemoveStoreFromBrand(
entry.brandName,
storeEntry.storeName
)
}
className="rounded-full p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-destructive"
aria-label={`删除店铺 ${storeEntry.storeName}`}
>
<Trash2 className="h-3.5 w-3.5" />
</button>
</div>
<span className="text-xs text-muted-foreground">
{(storeEntry.thirdCategories || []).length} 个三级分类
</span>
</div>
</div>
<div className="mt-3 rounded-xl border bg-muted/20 p-3">
<div className="flex flex-col gap-2 sm:flex-row">
<Input
value={thirdCategoryDrafts[draftKey] || ""}
onChange={(e) =>
setThirdCategoryDrafts((prev) => ({
...prev,
[draftKey]: e.target.value,
}))
}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
handleAddThirdCategoryToStore(
entry.brandName,
storeEntry.storeName
);
}
}}
placeholder={`给店铺“${storeEntry.storeName}”添加三级分类`}
/>
<Button
type="button"
onClick={() =>
handleAddThirdCategoryToStore(
entry.brandName,
storeEntry.storeName
)
}
className="shrink-0"
variant="outline"
>
<Plus className="mr-2 h-4 w-4" />
添加三级分类
</Button>
</div>
{(storeEntry.thirdCategories || []).length > 0 ? (
<div className="mt-3 flex flex-wrap gap-2">
{(storeEntry.thirdCategories || []).map(
(thirdCategory) => (
<div
key={`${entry.brandName}-${storeEntry.storeName}-${thirdCategory}`}
className="inline-flex items-center gap-2 rounded-full border bg-background px-3 py-1.5 text-sm"
>
<span>{thirdCategory}</span>
<button
type="button"
onClick={() =>
handleRemoveThirdCategoryFromStore(
entry.brandName,
storeEntry.storeName,
thirdCategory
)
}
className="rounded-full p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-destructive"
aria-label={`删除三级分类 ${thirdCategory}`}
>
<Trash2 className="h-3.5 w-3.5" />
</button>
</div>
)
)}
</div>
) : (
<div className="mt-3 rounded-lg border border-dashed bg-background/70 px-3 py-4 text-sm text-muted-foreground">
这个店铺下还没有添加任何三级分类。
</div>
)}
</div>
</div>
);
})}
</div>
) : (
<div className="mt-3 rounded-lg border border-dashed bg-background/70 px-3 py-4 text-sm text-muted-foreground">
@ -2594,7 +2931,7 @@ export function AgentWorkflowSettingsClient({
</div>
) : (
<div className="mt-3 rounded-lg border border-dashed bg-background/70 px-3 py-4 text-sm text-muted-foreground">
还没有添加品牌。先添加品牌,再在每个品牌下继续添加店铺。
还没有添加品牌。先添加品牌,再在每个品牌下继续添加店铺和三级分类。
</div>
)}
</div>

View File

@ -730,12 +730,26 @@ const updateSchema = z.object({
.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(),
priceBands: z.array(z.string().trim().min(1).max(64)).max(300).optional(),
brandStoreEntries: z
.array(
z.object({
brandName: z.string().trim().min(1).max(64),
stores: z.array(z.string().trim().min(1).max(64)).max(300).optional(),
storeEntries: z
.array(
z.object({
storeName: z.string().trim().min(1).max(64),
thirdCategories: 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(),
})
)

View File

@ -152,6 +152,7 @@ function hasMeaningfulWorkflowState(
String(inputs?.secondarySku || "").trim() ||
String(inputs?.brand || "").trim() ||
String(inputs?.store || "").trim() ||
String(inputs?.thirdCategory || "").trim() ||
String(inputs?.priceBand || "").trim()
);
@ -320,6 +321,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
secondarySku: "",
brand: "",
store: "",
thirdCategory: "",
priceBand: "",
});
const [workflowMaterials, setWorkflowMaterials] = useState<AgentWorkflowMaterialSelections>(
@ -486,6 +488,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
secondarySku: workflowInputs.secondarySku.trim(),
brand: workflowInputs.brand.trim(),
store: workflowInputs.store.trim(),
thirdCategory: workflowInputs.thirdCategory.trim(),
priceBand: workflowInputs.priceBand.trim(),
},
materials: workflowMaterials,
@ -531,6 +534,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
secondarySku: "",
brand: "",
store: "",
thirdCategory: "",
priceBand: "",
});
setWorkflowMaterials(createEmptyAgentWorkflowMaterialSelections());
@ -562,6 +566,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
secondarySku: String(persistedWorkflow.inputs?.secondarySku || "").trim(),
brand: String(persistedWorkflow.inputs?.brand || "").trim(),
store: String(persistedWorkflow.inputs?.store || "").trim(),
thirdCategory: String(persistedWorkflow.inputs?.thirdCategory || "").trim(),
priceBand: String(persistedWorkflow.inputs?.priceBand || "").trim(),
});
setWorkflowMaterials(
@ -670,6 +675,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
secondarySku: workflowInputs.secondarySku.trim(),
brand: workflowInputs.brand.trim(),
store: workflowInputs.store.trim(),
thirdCategory: workflowInputs.thirdCategory.trim(),
priceBand: workflowInputs.priceBand.trim(),
},
materials: workflowMaterials,
@ -756,6 +762,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
secondarySku: String(parsed.inputs?.secondarySku || "").trim(),
brand: String(parsed.inputs?.brand || "").trim(),
store: String(parsed.inputs?.store || "").trim(),
thirdCategory: String(parsed.inputs?.thirdCategory || "").trim(),
priceBand: String(parsed.inputs?.priceBand || "").trim(),
});
setWorkflowMaterials(normalizeAgentWorkflowMaterialSelections(parsed.materials));
@ -882,6 +889,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
secondarySku: String(persistedWorkflow.inputs?.secondarySku || "").trim(),
brand: String(persistedWorkflow.inputs?.brand || "").trim(),
store: String(persistedWorkflow.inputs?.store || "").trim(),
thirdCategory: String(persistedWorkflow.inputs?.thirdCategory || "").trim(),
priceBand: String(persistedWorkflow.inputs?.priceBand || "").trim(),
});
setWorkflowMaterials(normalizeAgentWorkflowMaterialSelections(persistedWorkflow.materials));

View File

@ -37,6 +37,7 @@ export function AgentPageDark({
secondarySku: "",
brand: "",
store: "",
thirdCategory: "",
priceBand: "",
});
const [workflowMaterials, setWorkflowMaterials] = useState<AgentWorkflowMaterialSelections>(

View File

@ -12,6 +12,7 @@ import {
defaultAgentWorkflowUiConfig,
getAgentWorkflowBrandStoreEntries,
getAgentWorkflowPriceBandOptions,
getAgentWorkflowThirdCategoryOptionsByBrandStore,
getAgentWorkflowStoreOptionsByBrand,
} from "@/lib/agents/workflow-ui-config";
import { createEmptyAgentWorkflowMaterialSelection } from "@/lib/agents/workflow-material-config";
@ -31,6 +32,7 @@ export interface AgentWorkflowInputs {
secondarySku: string;
brand: string;
store: string;
thirdCategory: string;
priceBand: string;
}
@ -98,6 +100,15 @@ export function AgentWorkflowPanel({
() => getAgentWorkflowStoreOptionsByBrand({ config, brand: inputs.brand }),
[config, inputs.brand]
);
const thirdCategoryOptions = useMemo(
() =>
getAgentWorkflowThirdCategoryOptionsByBrandStore({
config,
brand: inputs.brand,
store: inputs.store,
}),
[config, inputs.brand, inputs.store]
);
const priceBandOptions = useMemo(
() => getAgentWorkflowPriceBandOptions(config),
[config]
@ -181,13 +192,14 @@ export function AgentWorkflowPanel({
onReset();
return;
}
onInputsChange({
primarySku: "",
secondarySku: "",
brand: "",
store: "",
priceBand: "",
});
onInputsChange({
primarySku: "",
secondarySku: "",
brand: "",
store: "",
thirdCategory: "",
priceBand: "",
});
onMaterialsChange({
primary: createEmptyAgentWorkflowMaterialSelection(),
secondary: createEmptyAgentWorkflowMaterialSelection(),
@ -205,7 +217,7 @@ export function AgentWorkflowPanel({
<div className="space-y-5 p-4">
<section className="space-y-3">
<div className="text-sm font-medium">基础配置</div>
<div className="grid grid-cols-1 gap-3 md:grid-cols-3">
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-4">
<SearchableSelect
value={inputs.brand}
options={brandOptions}
@ -220,6 +232,10 @@ export function AgentWorkflowPanel({
value.trim().toLowerCase() === inputs.brand.trim().toLowerCase()
? inputs.store
: "",
thirdCategory:
value.trim().toLowerCase() === inputs.brand.trim().toLowerCase()
? inputs.thirdCategory
: "",
})
}
/>
@ -230,7 +246,25 @@ export function AgentWorkflowPanel({
searchPlaceholder="输入店铺名筛选"
emptyText={inputs.brand ? "当前品牌下暂无店铺可选" : "请先选择品牌"}
disabled={!inputs.brand || (brandStoreEntries.length > 0 && storeOptions.length === 0)}
onValueChange={(value) => onInputsChange({ ...inputs, store: value })}
onValueChange={(value) =>
onInputsChange({
...inputs,
store: value,
thirdCategory:
value.trim().toLowerCase() === inputs.store.trim().toLowerCase()
? inputs.thirdCategory
: "",
})
}
/>
<SearchableSelect
value={inputs.thirdCategory}
options={thirdCategoryOptions}
placeholder="选择三级分类"
searchPlaceholder="输入三级分类筛选"
emptyText={inputs.store ? "当前店铺下暂无三级分类" : "请先选择店铺"}
disabled={!inputs.store || thirdCategoryOptions.length === 0}
onValueChange={(value) => onInputsChange({ ...inputs, thirdCategory: value })}
/>
<SearchableSelect
value={inputs.priceBand}
@ -322,6 +356,7 @@ export function AgentWorkflowPanel({
inputs.secondarySku ||
inputs.brand ||
inputs.store ||
inputs.thirdCategory ||
inputs.priceBand ||
materials.primary.selectedImageIds.length > 0 ||
materials.secondary.selectedImageIds.length > 0 ? (
@ -338,6 +373,9 @@ export function AgentWorkflowPanel({
{inputs.store ? (
<span className="rounded-full bg-background px-2 py-1">店铺:{inputs.store}</span>
) : null}
{inputs.thirdCategory ? (
<span className="rounded-full bg-background px-2 py-1">三级分类:{inputs.thirdCategory}</span>
) : null}
{inputs.priceBand ? (
<span className="rounded-full bg-background px-2 py-1">价格带:{inputs.priceBand}</span>
) : null}

View File

@ -13,6 +13,7 @@ import {
defaultAgentWorkflowUiConfig,
getAgentWorkflowBrandStoreEntries,
getAgentWorkflowPriceBandOptions,
getAgentWorkflowThirdCategoryOptionsByBrandStore,
getAgentWorkflowStoreOptionsByBrand,
} from "@/lib/agents/workflow-ui-config";
import { createEmptyAgentWorkflowMaterialSelection } from "@/lib/agents/workflow-material-config";
@ -32,6 +33,7 @@ export interface AgentWorkflowInputs {
secondarySku: string;
brand: string;
store: string;
thirdCategory: string;
priceBand: string;
}
@ -180,6 +182,15 @@ export function AgentWorkflowPanelDark({
() => getAgentWorkflowStoreOptionsByBrand({ config, brand: inputs.brand }),
[config, inputs.brand]
);
const thirdCategoryOptions = useMemo(
() =>
getAgentWorkflowThirdCategoryOptionsByBrandStore({
config,
brand: inputs.brand,
store: inputs.store,
}),
[config, inputs.brand, inputs.store]
);
const priceBandOptions = useMemo(
() => getAgentWorkflowPriceBandOptions(config),
[config]
@ -273,7 +284,7 @@ export function AgentWorkflowPanelDark({
onReset();
return;
}
onInputsChange({ primarySku: "", secondarySku: "", brand: "", store: "", priceBand: "" });
onInputsChange({ primarySku: "", secondarySku: "", brand: "", store: "", thirdCategory: "", priceBand: "" });
onMaterialsChange({
primary: createEmptyAgentWorkflowMaterialSelection(),
secondary: createEmptyAgentWorkflowMaterialSelection(),
@ -307,7 +318,7 @@ export function AgentWorkflowPanelDark({
基础配置
</motion.h2>
<motion.div
className="grid grid-cols-1 gap-4 md:grid-cols-3"
className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
@ -326,6 +337,10 @@ export function AgentWorkflowPanelDark({
value.trim().toLowerCase() === inputs.brand.trim().toLowerCase()
? inputs.store
: "",
thirdCategory:
value.trim().toLowerCase() === inputs.brand.trim().toLowerCase()
? inputs.thirdCategory
: "",
})
}
/>
@ -336,7 +351,25 @@ export function AgentWorkflowPanelDark({
searchPlaceholder="输入店铺名筛选"
emptyText={inputs.brand ? "当前品牌下暂无店铺可选" : "请先选择品牌"}
disabled={!inputs.brand || (brandStoreEntries.length > 0 && storeOptions.length === 0)}
onValueChange={(value) => onInputsChange({ ...inputs, store: value })}
onValueChange={(value) =>
onInputsChange({
...inputs,
store: value,
thirdCategory:
value.trim().toLowerCase() === inputs.store.trim().toLowerCase()
? inputs.thirdCategory
: "",
})
}
/>
<SearchableSelect
value={inputs.thirdCategory}
options={thirdCategoryOptions}
placeholder="选择三级分类"
searchPlaceholder="输入三级分类筛选"
emptyText={inputs.store ? "当前店铺下暂无三级分类" : "请先选择店铺"}
disabled={!inputs.store || thirdCategoryOptions.length === 0}
onValueChange={(value) => onInputsChange({ ...inputs, thirdCategory: value })}
/>
<SearchableSelect
value={inputs.priceBand}

View File

@ -15,6 +15,7 @@ import {
getAgentWorkflowBlockedOptionKeysForSelections,
getAgentWorkflowBrandStoreEntries,
getAgentWorkflowPriceBandOptions,
getAgentWorkflowThirdCategoryOptionsByBrandStore,
getAgentWorkflowSelectedOptionPath,
getAgentWorkflowStoreOptionsByBrand,
getEnabledAgentWorkflowOptionTree,
@ -41,6 +42,7 @@ export interface AgentWorkflowInputs {
secondarySku: string;
brand: string;
store: string;
thirdCategory: string;
priceBand: string;
}
@ -271,8 +273,8 @@ export function AgentWorkflowBrandStorePanel({
inputs,
onInputsChange,
theme = "dark",
title = "品牌和店铺选择",
description = "先选择品牌、店铺和价格带,后续品牌模特与风格步骤会按这里的选择联动。",
title = "品牌层级选择",
description = "先选择品牌、店铺、三级分类和价格带,后续品牌模特与风格步骤会按这里的选择联动。",
}: AgentWorkflowBrandStorePanelProps) {
const brandOptions = useMemo(
() => config?.inputOptions?.brands || defaultAgentWorkflowUiConfig.inputOptions?.brands || [],
@ -286,6 +288,15 @@ export function AgentWorkflowBrandStorePanel({
() => getAgentWorkflowStoreOptionsByBrand({ config, brand: inputs.brand }),
[config, inputs.brand]
);
const thirdCategoryOptions = useMemo(
() =>
getAgentWorkflowThirdCategoryOptionsByBrandStore({
config,
brand: inputs.brand,
store: inputs.store,
}),
[config, inputs.brand, inputs.store]
);
const priceBandOptions = useMemo(
() => getAgentWorkflowPriceBandOptions(config),
[config]
@ -307,7 +318,7 @@ export function AgentWorkflowBrandStorePanel({
{description}
</div>
</div>
<div className="grid max-w-5xl grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
<div className="grid max-w-6xl grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
<SearchableSelect
value={inputs.brand}
options={brandOptions}
@ -323,6 +334,10 @@ export function AgentWorkflowBrandStorePanel({
value.trim().toLowerCase() === inputs.brand.trim().toLowerCase()
? inputs.store
: "",
thirdCategory:
value.trim().toLowerCase() === inputs.brand.trim().toLowerCase()
? inputs.thirdCategory
: "",
})
}
/>
@ -334,7 +349,26 @@ export function AgentWorkflowBrandStorePanel({
emptyText={inputs.brand ? "当前品牌下暂无店铺可选" : "请先选择品牌"}
theme={theme}
disabled={!inputs.brand || (brandStoreEntries.length > 0 && storeOptions.length === 0)}
onValueChange={(value) => onInputsChange({ ...inputs, store: value })}
onValueChange={(value) =>
onInputsChange({
...inputs,
store: value,
thirdCategory:
value.trim().toLowerCase() === inputs.store.trim().toLowerCase()
? inputs.thirdCategory
: "",
})
}
/>
<SearchableSelect
value={inputs.thirdCategory}
options={thirdCategoryOptions}
placeholder="选择三级分类"
searchPlaceholder="输入三级分类筛选"
emptyText={inputs.store ? "当前店铺下暂无三级分类" : "请先选择店铺"}
theme={theme}
disabled={!inputs.store || thirdCategoryOptions.length === 0}
onValueChange={(value) => onInputsChange({ ...inputs, thirdCategory: value })}
/>
<SearchableSelect
value={inputs.priceBand}

View File

@ -13,6 +13,7 @@ import {
defaultAgentWorkflowUiConfig,
getAgentWorkflowBrandStoreEntries,
getAgentWorkflowPriceBandOptions,
getAgentWorkflowThirdCategoryOptionsByBrandStore,
getAgentWorkflowStoreOptionsByBrand,
} from "@/lib/agents/workflow-ui-config";
import { createEmptyAgentWorkflowMaterialSelection } from "@/lib/agents/workflow-material-config";
@ -32,6 +33,7 @@ export interface AgentWorkflowInputs {
secondarySku: string;
brand: string;
store: string;
thirdCategory: string;
priceBand: string;
}
@ -192,6 +194,15 @@ export function AgentWorkflowPanelNew({
() => getAgentWorkflowStoreOptionsByBrand({ config, brand: inputs.brand }),
[config, inputs.brand]
);
const thirdCategoryOptions = useMemo(
() =>
getAgentWorkflowThirdCategoryOptionsByBrandStore({
config,
brand: inputs.brand,
store: inputs.store,
}),
[config, inputs.brand, inputs.store]
);
const priceBandOptions = useMemo(
() => getAgentWorkflowPriceBandOptions(config),
[config]
@ -294,7 +305,7 @@ export function AgentWorkflowPanelNew({
onReset();
return;
}
onInputsChange({ primarySku: "", secondarySku: "", brand: "", store: "", priceBand: "" });
onInputsChange({ primarySku: "", secondarySku: "", brand: "", store: "", thirdCategory: "", priceBand: "" });
onMaterialsChange({
primary: createEmptyAgentWorkflowMaterialSelection(),
secondary: createEmptyAgentWorkflowMaterialSelection(),
@ -346,7 +357,7 @@ export function AgentWorkflowPanelNew({
</span>
基础配置
</h3>
<div className="grid grid-cols-1 gap-3 md:grid-cols-3">
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-4">
<SearchableSelect
value={inputs.brand}
options={brandOptions}
@ -361,6 +372,10 @@ export function AgentWorkflowPanelNew({
value.trim().toLowerCase() === inputs.brand.trim().toLowerCase()
? inputs.store
: "",
thirdCategory:
value.trim().toLowerCase() === inputs.brand.trim().toLowerCase()
? inputs.thirdCategory
: "",
})
}
/>
@ -371,7 +386,25 @@ export function AgentWorkflowPanelNew({
searchPlaceholder="输入店铺名筛选"
emptyText={inputs.brand ? "当前品牌下暂无店铺可选" : "请先选择品牌"}
disabled={!inputs.brand || (brandStoreEntries.length > 0 && storeOptions.length === 0)}
onValueChange={(value) => onInputsChange({ ...inputs, store: value })}
onValueChange={(value) =>
onInputsChange({
...inputs,
store: value,
thirdCategory:
value.trim().toLowerCase() === inputs.store.trim().toLowerCase()
? inputs.thirdCategory
: "",
})
}
/>
<SearchableSelect
value={inputs.thirdCategory}
options={thirdCategoryOptions}
placeholder="选择三级分类"
searchPlaceholder="输入三级分类筛选"
emptyText={inputs.store ? "当前店铺下暂无三级分类" : "请先选择店铺"}
disabled={!inputs.store || thirdCategoryOptions.length === 0}
onValueChange={(value) => onInputsChange({ ...inputs, thirdCategory: value })}
/>
<SearchableSelect
value={inputs.priceBand}

View File

@ -34,6 +34,7 @@ export function createEmptyAgentWorkflowPersistedState(): AgentWorkflowPersisted
secondarySku: "",
brand: "",
store: "",
thirdCategory: "",
priceBand: "",
},
materials: createEmptyAgentWorkflowMaterialSelections(),
@ -229,6 +230,7 @@ function normalizeWorkflowState(value: unknown): AgentWorkflowPersistedState {
secondarySku: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).secondarySku || "" : "").trim(),
brand: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).brand || "" : "").trim(),
store: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).store || "" : "").trim(),
thirdCategory: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).thirdCategory || "" : "").trim(),
priceBand: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).priceBand || "" : "").trim(),
},
materials: normalizeAgentWorkflowMaterialSelections(raw.materials),

View File

@ -20,6 +20,7 @@ export interface AgentWorkflowInputsValue {
secondarySku: string;
brand: string;
store: string;
thirdCategory: string;
priceBand: string;
}
@ -212,6 +213,9 @@ export function buildAgentWorkflowComposerContext(params: {
if (params.inputs.store.trim()) {
summaryParts.push(`店铺:${params.inputs.store.trim()}`);
}
if (params.inputs.thirdCategory.trim()) {
summaryParts.push(`三级分类:${params.inputs.thirdCategory.trim()}`);
}
if (params.inputs.priceBand.trim()) {
summaryParts.push(`价格带:${params.inputs.priceBand.trim()}`);
}
@ -269,6 +273,9 @@ export function buildAgentWorkflowComposerContext(params: {
if (params.inputs.store.trim()) {
promptLines.push(`店铺:${params.inputs.store.trim()}`);
}
if (params.inputs.thirdCategory.trim()) {
promptLines.push(`三级分类:${params.inputs.thirdCategory.trim()}`);
}
if (params.inputs.priceBand.trim()) {
promptLines.push(`价格带:${params.inputs.priceBand.trim()}`);
}
@ -354,6 +361,7 @@ export function buildAgentWorkflowComposerContext(params: {
secondarySku: params.inputs.secondarySku.trim() || undefined,
brand: params.inputs.brand.trim() || undefined,
store: params.inputs.store.trim() || undefined,
thirdCategory: params.inputs.thirdCategory.trim() || undefined,
priceBand: params.inputs.priceBand.trim() || undefined,
selections,
referenceImages,

View File

@ -1,6 +1,7 @@
import type {
AgentWorkflowBrandStoreEntry,
AgentWorkflowOptionUiConfig,
AgentWorkflowStoreEntry,
AgentWorkflowStepKey,
AgentWorkflowStepUiConfig,
AgentWorkflowUiConfig,
@ -139,6 +140,7 @@ export const defaultAgentWorkflowUiConfig: AgentWorkflowUiConfig = {
inputOptions: {
brands: [],
stores: [],
thirdCategories: [],
priceBands: [],
brandStoreEntries: [],
},
@ -432,6 +434,58 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
const normalizeBrandStoreEntries = (input: unknown) => {
if (!Array.isArray(input)) return [] as AgentWorkflowBrandStoreEntry[];
const normalizeStoreEntries = (
rawStoreEntries: unknown,
legacyStores: string[]
): AgentWorkflowStoreEntry[] => {
const mappedEntries: Array<AgentWorkflowStoreEntry | null> = Array.isArray(rawStoreEntries)
? rawStoreEntries.map((storeItem, storeIndex) => {
if (typeof storeItem === "string") {
const storeName = normalizeText(storeItem, 64);
if (!storeName) return null;
return {
storeName,
thirdCategories: [],
sortOrder: storeIndex,
};
}
const storeRecord =
storeItem && typeof storeItem === "object" && !Array.isArray(storeItem)
? (storeItem as Record<string, unknown>)
: {};
const storeName = normalizeText(storeRecord.storeName, 64);
if (!storeName) return null;
return {
storeName,
thirdCategories: normalizeStringList(storeRecord.thirdCategories).slice(0, 300),
sortOrder: normalizeNumber(storeRecord.sortOrder, storeIndex),
};
})
: [];
const normalizedFromEntries = mappedEntries
.filter((item): item is AgentWorkflowStoreEntry => item !== null)
.sort((left, right) => (left.sortOrder || 0) - (right.sortOrder || 0))
.map((entry, index) => ({
...entry,
thirdCategories: Array.from(
new Set((entry.thirdCategories || []).map((item) => item.trim()).filter(Boolean))
).slice(0, 300),
sortOrder: index,
}));
if (normalizedFromEntries.length > 0) {
return normalizedFromEntries;
}
return legacyStores.map((storeName, index) => ({
storeName,
thirdCategories: [],
sortOrder: index,
}));
};
const mappedEntries: Array<AgentWorkflowBrandStoreEntry | null> = input.map((item, index) => {
const record =
item && typeof item === "object" && !Array.isArray(item)
@ -439,11 +493,14 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
: {};
const brandName = normalizeText(record.brandName, 64);
if (!brandName) return null;
const stores = normalizeStringList(record.stores).slice(0, 300);
const legacyStores = normalizeStringList(record.stores).slice(0, 300);
const storeEntries = normalizeStoreEntries(record.storeEntries, legacyStores);
const stores = storeEntries.map((entry) => entry.storeName);
return {
brandName,
stores,
storeEntries,
sortOrder: normalizeNumber(record.sortOrder, index),
};
});
@ -461,6 +518,7 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
);
const normalizedBrands = normalizeStringList(rawInputOptions.brands);
const normalizedStores = normalizeStringList(rawInputOptions.stores);
const normalizedThirdCategories = normalizeStringList(rawInputOptions.thirdCategories);
const normalizedPriceBands = normalizeStringList(rawInputOptions.priceBands);
const fallbackBrandStoreEntries: AgentWorkflowBrandStoreEntry[] =
normalizedBrandStoreEntries.length > 0
@ -468,6 +526,14 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
: normalizedBrands.map((brandName, index) => ({
brandName,
stores: normalizedBrands.length === 1 ? normalizedStores : [],
storeEntries:
normalizedBrands.length === 1
? normalizedStores.map((storeName, storeIndex) => ({
storeName,
thirdCategories: [],
sortOrder: storeIndex,
}))
: [],
sortOrder: index,
}));
const defaultStepByKey = new Map(
@ -633,9 +699,25 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
stores:
fallbackBrandStoreEntries.length > 0
? Array.from(
new Set(fallbackBrandStoreEntries.flatMap((entry) => entry.stores))
new Set(
fallbackBrandStoreEntries.flatMap((entry) =>
(entry.storeEntries || []).map((storeEntry) => storeEntry.storeName)
)
)
)
: normalizedStores,
thirdCategories:
fallbackBrandStoreEntries.length > 0
? Array.from(
new Set(
fallbackBrandStoreEntries.flatMap((entry) =>
(entry.storeEntries || []).flatMap(
(storeEntry) => storeEntry.thirdCategories || []
)
)
)
)
: normalizedThirdCategories,
priceBands: normalizedPriceBands,
brandStoreEntries: fallbackBrandStoreEntries,
},
@ -660,6 +742,12 @@ export function getAgentWorkflowStoreOptionsByBrand(params: {
const matchedEntry = entries.find(
(entry) => entry.brandName.trim().toLowerCase() === normalizedBrand
);
const storeOptions = matchedEntry
? (matchedEntry.storeEntries || []).map((entry) => entry.storeName).filter(Boolean)
: [];
if (storeOptions.length > 0) {
return storeOptions;
}
if (matchedEntry && matchedEntry.stores.length > 0) {
return matchedEntry.stores;
}
@ -667,6 +755,31 @@ export function getAgentWorkflowStoreOptionsByBrand(params: {
return params.config?.inputOptions?.stores || defaultAgentWorkflowUiConfig.inputOptions?.stores || [];
}
export function getAgentWorkflowThirdCategoryOptionsByBrandStore(params: {
config?: AgentWorkflowUiConfig | null;
brand?: string | null;
store?: string | null;
}) {
const entries = getAgentWorkflowBrandStoreEntries(params.config);
const normalizedBrand = String(params.brand || "").trim().toLowerCase();
const normalizedStore = String(params.store || "").trim().toLowerCase();
if (!normalizedBrand || !normalizedStore) {
return [];
}
const matchedEntry = entries.find(
(entry) => entry.brandName.trim().toLowerCase() === normalizedBrand
);
const matchedStoreEntry = matchedEntry?.storeEntries?.find(
(storeEntry) => storeEntry.storeName.trim().toLowerCase() === normalizedStore
);
if (matchedStoreEntry && matchedStoreEntry.thirdCategories.length > 0) {
return matchedStoreEntry.thirdCategories;
}
return params.config?.inputOptions?.thirdCategories || defaultAgentWorkflowUiConfig.inputOptions?.thirdCategories || [];
}
export function getAgentWorkflowPriceBandOptions(config?: AgentWorkflowUiConfig | null) {
return config?.inputOptions?.priceBands || defaultAgentWorkflowUiConfig.inputOptions?.priceBands || [];
}
@ -749,6 +862,24 @@ export function getAgentWorkflowBlockedOptionKeys(params: {
);
}
function collectAgentWorkflowOptionBranchKeys(
options: AgentWorkflowOptionUiConfig[],
targetKey: string
): string[] {
for (const option of options) {
if (option.key === targetKey) {
return flattenAgentWorkflowOptionTree([option]).map(({ option: currentOption }) => currentOption.key);
}
const childKeys = collectAgentWorkflowOptionBranchKeys(option.children || [], targetKey);
if (childKeys.length > 0) {
return childKeys;
}
}
return [];
}
export function getAgentWorkflowBlockedOptionKeysForSelections(params: {
config?: AgentWorkflowUiConfig | null;
selections: Partial<Record<AgentWorkflowStepKey, string>>;
@ -759,7 +890,7 @@ export function getAgentWorkflowBlockedOptionKeysForSelections(params: {
params.selections
).map(({ step }) => step);
return Array.from(
const directlyBlockedKeys = Array.from(
new Set(
visibleSteps.flatMap((step) =>
getAgentWorkflowBlockedOptionKeys({
@ -769,6 +900,18 @@ export function getAgentWorkflowBlockedOptionKeysForSelections(params: {
)
)
);
return Array.from(
new Set(
directlyBlockedKeys.flatMap((blockedKey) => {
const branchKeys = collectAgentWorkflowOptionBranchKeys(
visibleSteps.flatMap((step) => step.options || []),
blockedKey
);
return branchKeys.length > 0 ? branchKeys : [blockedKey];
})
)
);
}
export function isAgentWorkflowStepSelectionComplete(params: {

View File

@ -617,6 +617,8 @@ export const canvasProject = pgTable("CanvasProject", {
secondarySku?: string;
brand?: string;
store?: string;
thirdCategory?: string;
priceBand?: string;
};
materials?: {
primary?: {

View File

@ -105,15 +105,23 @@ export interface AgentWorkflowUiConfig {
inputOptions?: {
brands: string[];
stores: string[];
thirdCategories?: string[];
priceBands?: string[];
brandStoreEntries?: AgentWorkflowBrandStoreEntry[];
};
steps: AgentWorkflowStepUiConfig[];
}
export interface AgentWorkflowStoreEntry {
storeName: string;
thirdCategories: string[];
sortOrder?: number;
}
export interface AgentWorkflowBrandStoreEntry {
brandName: string;
stores: string[];
storeEntries?: AgentWorkflowStoreEntry[];
sortOrder?: number;
}
@ -249,6 +257,7 @@ export interface AgentWorkflowComposerContext {
secondarySku?: string;
brand?: string;
store?: string;
thirdCategory?: string;
priceBand?: string;
selections: AgentWorkflowComposerSelection[];
referenceImages: AgentWorkflowReferenceImage[];
@ -382,6 +391,7 @@ export interface AgentWorkflowPersistedState {
secondarySku: string;
brand: string;
store: string;
thirdCategory: string;
priceBand: string;
};
materials?: AgentWorkflowMaterialSelections;