feat: add workflow price bands and fix agent slots
This commit is contained in:
parent
8aa0595741
commit
01cce4ac55
@ -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>
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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 || {});
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -37,6 +37,7 @@ export function AgentPageDark({
|
||||
secondarySku: "",
|
||||
brand: "",
|
||||
store: "",
|
||||
priceBand: "",
|
||||
});
|
||||
const [workflowMaterials, setWorkflowMaterials] = useState<AgentWorkflowMaterialSelections>(
|
||||
createEmptyAgentWorkflowMaterialSelections()
|
||||
|
||||
@ -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} 张
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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(" / "),
|
||||
|
||||
@ -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(">")
|
||||
|
||||
@ -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>>;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user