feat: add workflow price bands and fix agent slots
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-03-23 13:48:37 +08:00
parent 8aa0595741
commit 01cce4ac55
13 changed files with 302 additions and 17 deletions

View File

@ -367,7 +367,8 @@ function createPresetLibrarySectionDraft(
}
function buildWorkflowInputOptionsFromBrandEntries(
entries: AgentWorkflowBrandStoreEntry[]
entries: AgentWorkflowBrandStoreEntry[],
priceBands: string[] = []
) {
const normalizedEntries = entries.map((entry, index) => ({
brandName: entry.brandName.trim(),
@ -380,11 +381,19 @@ function buildWorkflowInputOptionsFromBrandEntries(
),
sortOrder: index,
}));
const normalizedPriceBands = Array.from(
new Set(
priceBands
.map((item) => item.trim())
.filter(Boolean)
)
);
return {
brandStoreEntries: normalizedEntries,
brands: normalizedEntries.map((entry) => entry.brandName),
stores: Array.from(new Set(normalizedEntries.flatMap((entry) => entry.stores))),
priceBands: normalizedPriceBands,
};
}
@ -426,6 +435,7 @@ export function AgentWorkflowSettingsClient({
const [expandedSteps, setExpandedSteps] = useState<Set<string>>(new Set());
const [activeTab, setActiveTab] = useState("showcase");
const [brandDraft, setBrandDraft] = useState("");
const [priceBandDraft, setPriceBandDraft] = useState("");
const [storeDrafts, setStoreDrafts] = useState<Record<string, string>>({});
const fileInputRefs = useRef<Record<string, HTMLInputElement | null>>({});
@ -468,6 +478,13 @@ export function AgentWorkflowSettingsClient({
[],
[config.inputOptions?.brandStoreEntries]
);
const priceBandOptions = useMemo(
() =>
config.inputOptions?.priceBands ||
defaultAgentWorkflowUiConfig.inputOptions?.priceBands ||
[],
[config.inputOptions?.priceBands]
);
const configuredBrands = useMemo(
() => brandStoreEntries.map((entry) => entry.brandName),
[brandStoreEntries]
@ -493,7 +510,10 @@ export function AgentWorkflowSettingsClient({
const updateInputOptions = (nextEntries: AgentWorkflowBrandStoreEntry[]) => {
setConfig((prev) => ({
...prev,
inputOptions: buildWorkflowInputOptionsFromBrandEntries(nextEntries),
inputOptions: buildWorkflowInputOptionsFromBrandEntries(
nextEntries,
prev.inputOptions?.priceBands || []
),
}));
};
@ -576,6 +596,38 @@ export function AgentWorkflowSettingsClient({
);
};
const handleAddPriceBand = () => {
const nextValue = normalizeInputOptionValue(priceBandDraft);
if (!nextValue) {
toast.error("请输入价格带区间");
return;
}
if (priceBandOptions.some((item) => item.toLowerCase() === nextValue.toLowerCase())) {
toast.error("该价格带已存在");
return;
}
setConfig((prev) => ({
...prev,
inputOptions: buildWorkflowInputOptionsFromBrandEntries(
prev.inputOptions?.brandStoreEntries || [],
[...(prev.inputOptions?.priceBands || []), nextValue]
),
}));
setPriceBandDraft("");
};
const handleRemovePriceBand = (priceBand: string) => {
setConfig((prev) => ({
...prev,
inputOptions: buildWorkflowInputOptionsFromBrandEntries(
prev.inputOptions?.brandStoreEntries || [],
(prev.inputOptions?.priceBands || []).filter((item) => item !== priceBand)
),
}));
};
const enabledStepsCount = useMemo(
() => flattenAgentWorkflowSteps(sortedSteps).filter(({ step }) => step.enabled).length,
[sortedSteps]
@ -2149,7 +2201,7 @@ export function AgentWorkflowSettingsClient({
</div>
<div>
<CardTitle className="text-base">基础下拉选项</CardTitle>
<CardDescription>配置品牌与品牌下对应的店铺母子关系</CardDescription>
<CardDescription>配置品牌、店铺母子关系,以及第一页可选的价格带区间</CardDescription>
</div>
</div>
</CardHeader>
@ -2271,6 +2323,64 @@ export function AgentWorkflowSettingsClient({
)}
</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-xs">价格带区间</Label>
<span className="text-xs text-muted-foreground">
{priceBandOptions.length} 个区间
</span>
</div>
<div className="rounded-xl border bg-muted/10 p-3">
<div className="flex flex-col gap-2 sm:flex-row">
<Input
value={priceBandDraft}
onChange={(e) => setPriceBandDraft(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
handleAddPriceBand();
}
}}
placeholder="例如:0-999元 / 1000-1999元"
/>
<Button
type="button"
onClick={handleAddPriceBand}
className="shrink-0"
variant="outline"
>
<Plus className="mr-2 h-4 w-4" />
添加价格带
</Button>
</div>
{priceBandOptions.length > 0 ? (
<div className="mt-3 flex flex-wrap gap-2">
{priceBandOptions.map((priceBand) => (
<div
key={priceBand}
className="inline-flex items-center gap-2 rounded-full border bg-background px-3 py-1.5 text-sm"
>
<span>{priceBand}</span>
<button
type="button"
onClick={() => handleRemovePriceBand(priceBand)}
className="rounded-full p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-destructive"
aria-label={`删除价格带 ${priceBand}`}
>
<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>
</CardContent>
</Card>
</motion.div>

View File

@ -668,6 +668,7 @@ 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(),
priceBands: z.array(z.string().trim().min(1).max(64)).max(300).optional(),
brandStoreEntries: z
.array(
z.object({

View File

@ -232,6 +232,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
secondarySku: "",
brand: "",
store: "",
priceBand: "",
});
const [workflowMaterials, setWorkflowMaterials] = useState<AgentWorkflowMaterialSelections>(
() => createEmptyAgentWorkflowMaterialSelections()
@ -373,6 +374,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
secondarySku: "",
brand: "",
store: "",
priceBand: "",
});
setWorkflowMaterials(createEmptyAgentWorkflowMaterialSelections());
setWorkflowSelections({});
@ -400,6 +402,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(),
priceBand: String(persistedWorkflow.inputs?.priceBand || "").trim(),
});
setWorkflowMaterials(
normalizeAgentWorkflowMaterialSelections(persistedWorkflow.materials)
@ -504,6 +507,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
secondarySku: workflowInputs.secondarySku.trim(),
brand: workflowInputs.brand.trim(),
store: workflowInputs.store.trim(),
priceBand: workflowInputs.priceBand.trim(),
},
materials: workflowMaterials,
selections: workflowSelections,
@ -548,6 +552,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(),
priceBand: String(parsed.inputs?.priceBand || "").trim(),
});
setWorkflowMaterials(normalizeAgentWorkflowMaterialSelections(parsed.materials));
setWorkflowSelections(parsed.selections || {});
@ -706,6 +711,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(),
priceBand: String(persistedWorkflow.inputs?.priceBand || "").trim(),
});
setWorkflowMaterials(normalizeAgentWorkflowMaterialSelections(persistedWorkflow.materials));
setWorkflowSelections(persistedWorkflow.selections || {});

View File

@ -170,6 +170,25 @@ function arePickedAttachmentsEqual(
return true;
}
function getAttachmentIdentityKey(
attachment:
| PickedAttachment
| {
url?: string | null;
assetId?: string | null;
}
| null
| undefined
) {
const assetId = String(attachment?.assetId || "").trim();
if (assetId) {
return `asset:${assetId}`;
}
const url = String(attachment?.url || "").trim();
return url ? `url:${url}` : null;
}
type ToolStepKey =
| "preset"
| "brand-model"
@ -489,6 +508,7 @@ export function AgentGenerationWorkspaceDarkV2({
const hiddenPromptRef = useRef<string | null>(null);
const generatedImageKeysRef = useRef<Set<string>>(new Set());
const lastSyncedBrandModelUrlRef = useRef<string | null>(null);
const lastSyncedProductSlotKeysRef = useRef<Array<string | null>>([]);
const workspaceStateSignatureRef = useRef<string>("");
const [localAgentModeState, setLocalAgentModeState] = useState<AgentModeWorkspaceState>(
() => createEmptyAgentModeWorkspaceState()
@ -639,8 +659,49 @@ export function AgentGenerationWorkspaceDarkV2({
const skuReferenceSourceLabel = getTaskDetailReferenceSourceLabel(skuReferenceTarget);
const workflowStoreName = String(workflowContext?.store || "").trim();
const shouldAllowModelOverride = !isSceneGenerationPreset || presetModeConfig.scene.allowModelOverride;
const selectedProductAttachments = attachments.filter((item, index) =>
modelSlotIndex >= 0 ? index !== modelSlotIndex && Boolean(item) : Boolean(item)
const brandModelAttachmentKeys = useMemo(() => {
const keys = new Set<string>();
referenceImages
.filter((image) => image.source === "brandModel")
.forEach((image) => {
const key = getAttachmentIdentityKey(image);
if (key) {
keys.add(key);
}
});
const selectedBrandModelKey = getAttachmentIdentityKey({
url: selectedBrandModel?.imageUrl || null,
assetId: selectedBrandModel?.assetId || null,
});
if (selectedBrandModelKey) {
keys.add(selectedBrandModelKey);
}
const currentModelSlotKey =
modelSlotIndex >= 0 ? getAttachmentIdentityKey(attachments[modelSlotIndex]) : null;
if (currentModelSlotKey) {
keys.add(currentModelSlotKey);
}
return keys;
}, [
attachments,
modelSlotIndex,
referenceImages,
selectedBrandModel?.assetId,
selectedBrandModel?.imageUrl,
]);
const selectedProductAttachments = useMemo(
() =>
attachments
.slice(0, productSlotCount)
.filter((item): item is PickedAttachment => {
if (!item) return false;
const attachmentKey = getAttachmentIdentityKey(item);
return !attachmentKey || !brandModelAttachmentKeys.has(attachmentKey);
}),
[attachments, brandModelAttachmentKeys, productSlotCount]
);
const selectedModelAttachment =
modelSlotIndex >= 0 && attachments[modelSlotIndex] ? attachments[modelSlotIndex] : null;
@ -790,6 +851,7 @@ export function AgentGenerationWorkspaceDarkV2({
useEffect(() => {
if (!hasAgentUploadSlots) {
setAttachments((current) => (current.length === 0 ? current : []));
lastSyncedProductSlotKeysRef.current = [];
return;
}
const nextSlots: Array<PickedAttachment | null> = Array.from(
@ -817,15 +879,34 @@ export function AgentGenerationWorkspaceDarkV2({
{ length: agentUploadSlots.length },
(_, index) => current[index] || null
);
for (let index = 0; index < agentUploadSlots.length; index += 1) {
if (!result[index] && nextSlots[index]) {
result[index] = nextSlots[index] as PickedAttachment;
for (let index = 0; index < productSlotCount; index += 1) {
const currentSlot = result[index];
const nextAttachment = nextSlots[index];
const currentSlotKey = getAttachmentIdentityKey(currentSlot);
const previousSyncedKey = lastSyncedProductSlotKeysRef.current[index] || null;
const currentIsPreviousAutoSelection =
Boolean(previousSyncedKey) && currentSlotKey === previousSyncedKey;
const currentIsBrandModel =
currentSlotKey ? brandModelAttachmentKeys.has(currentSlotKey) : false;
if (nextAttachment) {
if (!currentSlot || currentIsPreviousAutoSelection || currentIsBrandModel) {
result[index] = nextAttachment as PickedAttachment;
}
} else if (currentIsPreviousAutoSelection || currentIsBrandModel) {
result[index] = null;
}
}
return arePickedAttachmentsEqual(current, result) ? current : result;
});
lastSyncedProductSlotKeysRef.current = Array.from(
{ length: productSlotCount },
(_, index) => getAttachmentIdentityKey(nextSlots[index])
);
}, [
agentUploadSlots.length,
brandModelAttachmentKeys,
hasAgentUploadSlots,
modelSlotEnabled,
modelSlotIndex,
@ -2082,13 +2163,13 @@ export function AgentGenerationWorkspaceDarkV2({
<span className="text-sm font-semibold text-white">风格步骤卡片</span>
</div>
<p className="text-sm text-white/45">
在同一页完成风格卡片、品牌模特和 Prompt 组合,不再跳转第二个页面。
先完成风格步骤卡片,再继续选择品牌模特,方便后续做人群联动,不再跳转第二个页面。
</p>
</div>
<div className="space-y-4">
{hasBrandModelStep ? brandModelPanel : null}
{workflowStylePanel}
{hasBrandModelStep ? brandModelPanel : null}
</div>
</section>
) : null}

View File

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

View File

@ -11,6 +11,7 @@ import { cn } from "@/lib/utils";
import {
defaultAgentWorkflowUiConfig,
getAgentWorkflowBrandStoreEntries,
getAgentWorkflowPriceBandOptions,
getAgentWorkflowStoreOptionsByBrand,
} from "@/lib/agents/workflow-ui-config";
import { createEmptyAgentWorkflowMaterialSelection } from "@/lib/agents/workflow-material-config";
@ -30,6 +31,7 @@ export interface AgentWorkflowInputs {
secondarySku: string;
brand: string;
store: string;
priceBand: string;
}
interface AgentWorkflowPanelProps {
@ -96,6 +98,10 @@ export function AgentWorkflowPanel({
() => getAgentWorkflowStoreOptionsByBrand({ config, brand: inputs.brand }),
[config, inputs.brand]
);
const priceBandOptions = useMemo(
() => getAgentWorkflowPriceBandOptions(config),
[config]
);
const resolvedConfig = useMemo(() => {
const source = config || defaultAgentWorkflowUiConfig;
return {
@ -180,6 +186,7 @@ export function AgentWorkflowPanel({
secondarySku: "",
brand: "",
store: "",
priceBand: "",
});
onMaterialsChange({
primary: createEmptyAgentWorkflowMaterialSelection(),
@ -198,7 +205,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">
<div className="grid grid-cols-1 gap-3 md:grid-cols-3">
<SearchableSelect
value={inputs.brand}
options={brandOptions}
@ -225,6 +232,15 @@ export function AgentWorkflowPanel({
disabled={!inputs.brand || (brandStoreEntries.length > 0 && storeOptions.length === 0)}
onValueChange={(value) => onInputsChange({ ...inputs, store: value })}
/>
<SearchableSelect
value={inputs.priceBand}
options={priceBandOptions}
placeholder="价格带"
searchPlaceholder="输入价格带筛选"
emptyText="暂无价格带可选"
disabled={priceBandOptions.length === 0}
onValueChange={(value) => onInputsChange({ ...inputs, priceBand: value })}
/>
</div>
{(brandOptions.length > 0 || storeOptions.length > 0) && (
<div className="text-xs text-muted-foreground">
@ -306,6 +322,7 @@ export function AgentWorkflowPanel({
inputs.secondarySku ||
inputs.brand ||
inputs.store ||
inputs.priceBand ||
materials.primary.selectedImageIds.length > 0 ||
materials.secondary.selectedImageIds.length > 0 ? (
<div className="flex flex-wrap gap-2 text-xs text-muted-foreground">
@ -321,6 +338,9 @@ export function AgentWorkflowPanel({
{inputs.store ? (
<span className="rounded-full bg-background px-2 py-1">店铺:{inputs.store}</span>
) : null}
{inputs.priceBand ? (
<span className="rounded-full bg-background px-2 py-1">价格带:{inputs.priceBand}</span>
) : null}
{materials.primary.selectedImageIds.length > 0 ? (
<span className="rounded-full bg-background px-2 py-1">
主SPU图:已选 {materials.primary.selectedImageIds.length} 张

View File

@ -12,6 +12,7 @@ import { cn } from "@/lib/utils";
import {
defaultAgentWorkflowUiConfig,
getAgentWorkflowBrandStoreEntries,
getAgentWorkflowPriceBandOptions,
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;
priceBand: string;
}
interface AgentWorkflowPanelDarkProps {
@ -178,6 +180,10 @@ export function AgentWorkflowPanelDark({
() => getAgentWorkflowStoreOptionsByBrand({ config, brand: inputs.brand }),
[config, inputs.brand]
);
const priceBandOptions = useMemo(
() => getAgentWorkflowPriceBandOptions(config),
[config]
);
const resolvedConfig = useMemo(() => {
const source = config || defaultAgentWorkflowUiConfig;
@ -267,7 +273,7 @@ export function AgentWorkflowPanelDark({
onReset();
return;
}
onInputsChange({ primarySku: "", secondarySku: "", brand: "", store: "" });
onInputsChange({ primarySku: "", secondarySku: "", brand: "", store: "", priceBand: "" });
onMaterialsChange({
primary: createEmptyAgentWorkflowMaterialSelection(),
secondary: createEmptyAgentWorkflowMaterialSelection(),
@ -301,7 +307,7 @@ export function AgentWorkflowPanelDark({
基础配置
</motion.h2>
<motion.div
className="grid grid-cols-1 gap-4 md:grid-cols-2"
className="grid grid-cols-1 gap-4 md:grid-cols-3"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
@ -332,6 +338,15 @@ export function AgentWorkflowPanelDark({
disabled={!inputs.brand || (brandStoreEntries.length > 0 && storeOptions.length === 0)}
onValueChange={(value) => onInputsChange({ ...inputs, store: value })}
/>
<SearchableSelect
value={inputs.priceBand}
options={priceBandOptions}
placeholder="选择价格带"
searchPlaceholder="输入价格带筛选"
emptyText="暂无价格带可选"
disabled={priceBandOptions.length === 0}
onValueChange={(value) => onInputsChange({ ...inputs, priceBand: value })}
/>
</motion.div>
</section>

View File

@ -13,6 +13,7 @@ import {
flattenAgentWorkflowSteps,
flattenVisibleAgentWorkflowSteps,
getAgentWorkflowBrandStoreEntries,
getAgentWorkflowPriceBandOptions,
getAgentWorkflowSelectedOptionPath,
getAgentWorkflowStoreOptionsByBrand,
getEnabledAgentWorkflowOptionTree,
@ -39,6 +40,7 @@ export interface AgentWorkflowInputs {
secondarySku: string;
brand: string;
store: string;
priceBand: string;
}
interface AgentWorkflowPanelDarkV2Props {
@ -202,7 +204,7 @@ export function AgentWorkflowBrandStorePanel({
onInputsChange,
theme = "dark",
title = "品牌和店铺选择",
description = "先选择品牌和店铺,后续品牌模特与风格步骤会按这里的选择联动。",
description = "先选择品牌、店铺和价格带,后续品牌模特与风格步骤会按这里的选择联动。",
}: AgentWorkflowBrandStorePanelProps) {
const brandOptions = useMemo(
() => config?.inputOptions?.brands || defaultAgentWorkflowUiConfig.inputOptions?.brands || [],
@ -216,6 +218,10 @@ export function AgentWorkflowBrandStorePanel({
() => getAgentWorkflowStoreOptionsByBrand({ config, brand: inputs.brand }),
[config, inputs.brand]
);
const priceBandOptions = useMemo(
() => getAgentWorkflowPriceBandOptions(config),
[config]
);
const isDark = theme === "dark";
return (
@ -233,7 +239,7 @@ export function AgentWorkflowBrandStorePanel({
{description}
</div>
</div>
<div className="grid max-w-2xl grid-cols-1 gap-4 md:grid-cols-2">
<div className="grid max-w-5xl grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
<SearchableSelect
value={inputs.brand}
options={brandOptions}
@ -262,6 +268,16 @@ export function AgentWorkflowBrandStorePanel({
disabled={!inputs.brand || (brandStoreEntries.length > 0 && storeOptions.length === 0)}
onValueChange={(value) => onInputsChange({ ...inputs, store: value })}
/>
<SearchableSelect
value={inputs.priceBand}
options={priceBandOptions}
placeholder="选择价格带"
searchPlaceholder="输入价格带筛选"
emptyText="暂无价格带可选"
theme={theme}
disabled={priceBandOptions.length === 0}
onValueChange={(value) => onInputsChange({ ...inputs, priceBand: value })}
/>
</div>
</div>
);

View File

@ -12,6 +12,7 @@ import { cn } from "@/lib/utils";
import {
defaultAgentWorkflowUiConfig,
getAgentWorkflowBrandStoreEntries,
getAgentWorkflowPriceBandOptions,
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;
priceBand: string;
}
interface AgentWorkflowPanelNewProps {
@ -190,6 +192,10 @@ export function AgentWorkflowPanelNew({
() => getAgentWorkflowStoreOptionsByBrand({ config, brand: inputs.brand }),
[config, inputs.brand]
);
const priceBandOptions = useMemo(
() => getAgentWorkflowPriceBandOptions(config),
[config]
);
const resolvedConfig = useMemo(() => {
const source = config || defaultAgentWorkflowUiConfig;
@ -288,7 +294,7 @@ export function AgentWorkflowPanelNew({
onReset();
return;
}
onInputsChange({ primarySku: "", secondarySku: "", brand: "", store: "" });
onInputsChange({ primarySku: "", secondarySku: "", brand: "", store: "", priceBand: "" });
onMaterialsChange({
primary: createEmptyAgentWorkflowMaterialSelection(),
secondary: createEmptyAgentWorkflowMaterialSelection(),
@ -340,7 +346,7 @@ export function AgentWorkflowPanelNew({
</span>
基础配置
</h3>
<div className="grid grid-cols-1 gap-3">
<div className="grid grid-cols-1 gap-3 md:grid-cols-3">
<SearchableSelect
value={inputs.brand}
options={brandOptions}
@ -367,6 +373,15 @@ export function AgentWorkflowPanelNew({
disabled={!inputs.brand || (brandStoreEntries.length > 0 && storeOptions.length === 0)}
onValueChange={(value) => onInputsChange({ ...inputs, store: value })}
/>
<SearchableSelect
value={inputs.priceBand}
options={priceBandOptions}
placeholder="选择价格带"
searchPlaceholder="输入价格带筛选"
emptyText="暂无价格带可选"
disabled={priceBandOptions.length === 0}
onValueChange={(value) => onInputsChange({ ...inputs, priceBand: value })}
/>
</div>
</motion.section>

View File

@ -32,6 +32,7 @@ export function createEmptyAgentWorkflowPersistedState(): AgentWorkflowPersisted
secondarySku: "",
brand: "",
store: "",
priceBand: "",
},
materials: createEmptyAgentWorkflowMaterialSelections(),
selections: {},
@ -106,6 +107,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(),
priceBand: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).priceBand || "" : "").trim(),
},
materials: normalizeAgentWorkflowMaterialSelections(raw.materials),
selections:

View File

@ -19,6 +19,7 @@ export interface AgentWorkflowInputsValue {
secondarySku: string;
brand: string;
store: string;
priceBand: string;
}
export function buildAgentWorkflowComposerContext(params: {
@ -70,6 +71,9 @@ export function buildAgentWorkflowComposerContext(params: {
if (params.inputs.store.trim()) {
summaryParts.push(`店铺:${params.inputs.store.trim()}`);
}
if (params.inputs.priceBand.trim()) {
summaryParts.push(`价格带:${params.inputs.priceBand.trim()}`);
}
if (referenceImages.some((item) => item.source === "primary")) {
summaryParts.push(
`主SPU图:${referenceImages.filter((item) => item.source === "primary").length}张`
@ -106,6 +110,9 @@ export function buildAgentWorkflowComposerContext(params: {
if (params.inputs.store.trim()) {
promptLines.push(`店铺:${params.inputs.store.trim()}`);
}
if (params.inputs.priceBand.trim()) {
promptLines.push(`价格带:${params.inputs.priceBand.trim()}`);
}
if (referenceImages.some((item) => item.source === "primary")) {
const labels = referenceImages
.filter((item) => item.source === "primary")
@ -151,6 +158,7 @@ export function buildAgentWorkflowComposerContext(params: {
secondarySku: params.inputs.secondarySku.trim() || undefined,
brand: params.inputs.brand.trim() || undefined,
store: params.inputs.store.trim() || undefined,
priceBand: params.inputs.priceBand.trim() || undefined,
selections,
referenceImages,
summaryText: summaryParts.join(" / "),

View File

@ -133,6 +133,7 @@ export const defaultAgentWorkflowUiConfig: AgentWorkflowUiConfig = {
inputOptions: {
brands: [],
stores: [],
priceBands: [],
brandStoreEntries: [],
},
steps: defaultSteps.map((step, index) =>
@ -402,6 +403,7 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
);
const normalizedBrands = normalizeStringList(rawInputOptions.brands);
const normalizedStores = normalizeStringList(rawInputOptions.stores);
const normalizedPriceBands = normalizeStringList(rawInputOptions.priceBands);
const fallbackBrandStoreEntries: AgentWorkflowBrandStoreEntry[] =
normalizedBrandStoreEntries.length > 0
? normalizedBrandStoreEntries
@ -571,6 +573,7 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
new Set(fallbackBrandStoreEntries.flatMap((entry) => entry.stores))
)
: normalizedStores,
priceBands: normalizedPriceBands,
brandStoreEntries: fallbackBrandStoreEntries,
},
steps: normalizedSteps,
@ -601,6 +604,10 @@ export function getAgentWorkflowStoreOptionsByBrand(params: {
return params.config?.inputOptions?.stores || defaultAgentWorkflowUiConfig.inputOptions?.stores || [];
}
export function getAgentWorkflowPriceBandOptions(config?: AgentWorkflowUiConfig | null) {
return config?.inputOptions?.priceBands || defaultAgentWorkflowUiConfig.inputOptions?.priceBands || [];
}
export function parseAgentWorkflowSelectionPath(value?: string | null) {
return String(value || "")
.split(">")

View File

@ -101,6 +101,7 @@ export interface AgentWorkflowUiConfig {
inputOptions?: {
brands: string[];
stores: string[];
priceBands?: string[];
brandStoreEntries?: AgentWorkflowBrandStoreEntry[];
};
steps: AgentWorkflowStepUiConfig[];
@ -243,6 +244,7 @@ export interface AgentWorkflowComposerContext {
secondarySku?: string;
brand?: string;
store?: string;
priceBand?: string;
selections: AgentWorkflowComposerSelection[];
referenceImages: AgentWorkflowReferenceImage[];
summaryText: string;
@ -321,6 +323,7 @@ export interface AgentWorkflowPersistedState {
secondarySku: string;
brand: string;
store: string;
priceBand: string;
};
materials?: AgentWorkflowMaterialSelections;
selections: Partial<Record<AgentWorkflowStepKey, string>>;