fix: sync ai detail material slots with workflow
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-25 15:48:21 +08:00
parent 01e124ebb1
commit f97e96c5f9

View File

@ -859,6 +859,9 @@ export function AgentGenerationWorkspaceDarkV2({
const generatedImageKeysRef = useRef<Set<string>>(new Set());
const lastSyncedBrandModelUrlRef = useRef<string | null>(null);
const lastSyncedProductSlotKeysRef = useRef<Array<string | null>>([]);
const lastSyncedDetailProductSlotKeysRef = useRef<Array<string | null>>([]);
const lastSyncedDetailBundleSlotKeysRef = useRef<Array<string | null>>([]);
const lastSyncedDetailModelSlotKeyRef = useRef<string | null>(null);
const detailGenerationCancelRef = useRef(false);
const workspaceStateSignatureRef = useRef<string>("");
const [localAgentModeState, setLocalAgentModeState] = useState<AgentModeWorkspaceState>(
@ -1147,6 +1150,30 @@ export function AgentGenerationWorkspaceDarkV2({
detailReplacementState.slots.length === AGENT_DETAIL_SLOT_CONFIGS.length
? detailReplacementState.slots
: createEmptyAgentDetailSlots();
const detailGenerationRequiredProductCount = Math.max(
1,
Math.min(
isDetailReplacementPreset
? presetModeConfig.detail.productImageSlotCount
: productSlotCount,
AGENT_DETAIL_PRODUCT_SLOT_INDEXES.length
)
);
const detailGenerationProductSlotIndexes = useMemo(
() =>
AGENT_DETAIL_PRODUCT_SLOT_INDEXES.slice(0, detailGenerationRequiredProductCount),
[detailGenerationRequiredProductCount]
);
const detailGenerationVisibleSlotIndexes = useMemo(
() => [
...detailGenerationProductSlotIndexes,
...(modelSlotEnabled && AGENT_DETAIL_MODEL_SLOT_INDEX >= 0
? [AGENT_DETAIL_MODEL_SLOT_INDEX]
: []),
...AGENT_DETAIL_BUNDLE_SLOT_INDEXES,
],
[detailGenerationProductSlotIndexes, modelSlotEnabled]
);
const detailGenerationProgress =
(detailReplacementState.progress || {
status: "idle",
@ -1156,14 +1183,15 @@ export function AgentGenerationWorkspaceDarkV2({
startedAt: null,
finishedAt: null,
}) as AgentDetailGenerationProgressState;
const detailGenerationProductCount = AGENT_DETAIL_PRODUCT_SLOT_INDEXES.filter(
const detailGenerationProductCount = detailGenerationProductSlotIndexes.filter(
(index) => Boolean(detailGenerationSlots[index]?.url)
).length;
const detailGenerationBundleCount = AGENT_DETAIL_BUNDLE_SLOT_INDEXES.filter(
(index) => Boolean(detailGenerationSlots[index]?.url)
).length;
const detailGenerationModelReady = Boolean(
AGENT_DETAIL_MODEL_SLOT_INDEX >= 0 &&
modelSlotEnabled &&
AGENT_DETAIL_MODEL_SLOT_INDEX >= 0 &&
detailGenerationSlots[AGENT_DETAIL_MODEL_SLOT_INDEX]?.url
);
const detailGenerationValidationIssues = useMemo(() => {
@ -1171,10 +1199,12 @@ export function AgentGenerationWorkspaceDarkV2({
if (!detailGenerationBrief.targetAudience) issues.push("缺少主购买人群");
if (!detailGenerationBrief.usageScenario) issues.push("缺少需求场景");
if (!detailGenerationBrief.coreNeed) issues.push("缺少核心需求");
if (detailGenerationProductCount < 5) {
issues.push(`产品图不足(${detailGenerationProductCount}/5)`);
if (detailGenerationProductCount < detailGenerationRequiredProductCount) {
issues.push(
`产品图不足(${detailGenerationProductCount}/${detailGenerationRequiredProductCount})`
);
}
if (!detailGenerationModelReady) {
if (modelSlotEnabled && !detailGenerationModelReady) {
issues.push("缺少模特图(1/1)");
}
return issues;
@ -1184,6 +1214,8 @@ export function AgentGenerationWorkspaceDarkV2({
detailGenerationBrief.usageScenario,
detailGenerationModelReady,
detailGenerationProductCount,
detailGenerationRequiredProductCount,
modelSlotEnabled,
]);
const skuReplacementState = resolvedAgentModeState.skuReplacement;
const skuReplacementQueryInput = skuReplacementState.query;
@ -2545,12 +2577,32 @@ export function AgentGenerationWorkspaceDarkV2({
);
useEffect(() => {
if (!isDetailReplacementPreset) {
lastSyncedDetailProductSlotKeysRef.current = [];
lastSyncedDetailBundleSlotKeysRef.current = [];
lastSyncedDetailModelSlotKeyRef.current = null;
return;
}
const secondaryReferenceImages = referenceImages
.filter((image) => image.source === "secondary")
.slice(0, AGENT_DETAIL_BUNDLE_SLOT_INDEXES.length);
const nextDetailProductSourceKeys = detailGenerationProductSlotIndexes.map((_, index) =>
getAttachmentIdentityKey(selectedProductAttachments[index])
);
const nextDetailBundleSourceKeys = AGENT_DETAIL_BUNDLE_SLOT_INDEXES.map((_, index) =>
getAttachmentIdentityKey(
secondaryReferenceImages[index]
? {
url: secondaryReferenceImages[index]?.url,
assetId: secondaryReferenceImages[index]?.assetId || null,
}
: null
)
);
const nextDetailModelSourceKey =
modelSlotEnabled && AGENT_DETAIL_MODEL_SLOT_INDEX >= 0
? getAttachmentIdentityKey(selectedModelAttachment)
: null;
updateDetailReplacementState((current) => {
let changed = false;
@ -2572,23 +2624,41 @@ export function AgentGenerationWorkspaceDarkV2({
{ length: AGENT_DETAIL_SLOT_CONFIGS.length },
(_, index) => current.slots?.[index] || null
);
AGENT_DETAIL_PRODUCT_SLOT_INDEXES.forEach((slotIndex, index) => {
detailGenerationProductSlotIndexes.forEach((slotIndex, index) => {
const attachment = selectedProductAttachments[index];
if (!attachment || nextSlots[slotIndex]) return;
nextSlots[slotIndex] = {
url: attachment.url,
name: attachment.name || AGENT_DETAIL_SLOT_CONFIGS[slotIndex]?.label || null,
contentType: attachment.contentType || "image/jpeg",
assetId: attachment.assetId || null,
};
changed = true;
const currentSlot = nextSlots[slotIndex];
const currentSlotKey = getAttachmentIdentityKey(currentSlot);
const previousSyncedKey = lastSyncedDetailProductSlotKeysRef.current[index] || null;
const currentIsPreviousAutoSelection =
Boolean(previousSyncedKey) && currentSlotKey === previousSyncedKey;
if (attachment) {
const nextAttachment = {
url: attachment.url,
name: attachment.name || AGENT_DETAIL_SLOT_CONFIGS[slotIndex]?.label || null,
contentType: attachment.contentType || "image/jpeg",
assetId: attachment.assetId || null,
};
if (!currentSlot || currentIsPreviousAutoSelection) {
nextSlots[slotIndex] = nextAttachment;
changed = true;
}
} else if (currentIsPreviousAutoSelection) {
nextSlots[slotIndex] = null;
changed = true;
}
});
if (
modelSlotEnabled &&
AGENT_DETAIL_MODEL_SLOT_INDEX >= 0 &&
selectedModelAttachment &&
!nextSlots[AGENT_DETAIL_MODEL_SLOT_INDEX]
selectedModelAttachment
) {
nextSlots[AGENT_DETAIL_MODEL_SLOT_INDEX] = {
const currentSlot = nextSlots[AGENT_DETAIL_MODEL_SLOT_INDEX];
const currentSlotKey = getAttachmentIdentityKey(currentSlot);
const previousSyncedKey = lastSyncedDetailModelSlotKeyRef.current;
const currentIsPreviousAutoSelection =
Boolean(previousSyncedKey) && currentSlotKey === previousSyncedKey;
const nextAttachment = {
url: selectedModelAttachment.url,
name:
selectedModelAttachment.name ||
@ -2597,28 +2667,61 @@ export function AgentGenerationWorkspaceDarkV2({
contentType: selectedModelAttachment.contentType || "image/jpeg",
assetId: selectedModelAttachment.assetId || null,
};
changed = true;
if (!currentSlot || currentIsPreviousAutoSelection) {
nextSlots[AGENT_DETAIL_MODEL_SLOT_INDEX] = nextAttachment;
changed = true;
}
} else if (modelSlotEnabled && AGENT_DETAIL_MODEL_SLOT_INDEX >= 0) {
const currentSlot = nextSlots[AGENT_DETAIL_MODEL_SLOT_INDEX];
const currentSlotKey = getAttachmentIdentityKey(currentSlot);
const previousSyncedKey = lastSyncedDetailModelSlotKeyRef.current;
const currentIsPreviousAutoSelection =
Boolean(previousSyncedKey) && currentSlotKey === previousSyncedKey;
if (currentIsPreviousAutoSelection) {
nextSlots[AGENT_DETAIL_MODEL_SLOT_INDEX] = null;
changed = true;
}
}
AGENT_DETAIL_BUNDLE_SLOT_INDEXES.forEach((slotIndex, index) => {
const image = secondaryReferenceImages[index];
if (!image || nextSlots[slotIndex]) return;
nextSlots[slotIndex] = {
url: image.url,
name:
image.name ||
image.label ||
AGENT_DETAIL_SLOT_CONFIGS[slotIndex]?.label ||
null,
contentType: image.mimeType || "image/jpeg",
assetId: image.assetId || null,
};
changed = true;
const currentSlot = nextSlots[slotIndex];
const currentSlotKey = getAttachmentIdentityKey(currentSlot);
const previousSyncedKey = lastSyncedDetailBundleSlotKeysRef.current[index] || null;
const currentIsPreviousAutoSelection =
Boolean(previousSyncedKey) && currentSlotKey === previousSyncedKey;
if (image) {
const nextAttachment = {
url: image.url,
name:
image.name ||
image.label ||
AGENT_DETAIL_SLOT_CONFIGS[slotIndex]?.label ||
null,
contentType: image.mimeType || "image/jpeg",
assetId: image.assetId || null,
};
if (!currentSlot || currentIsPreviousAutoSelection) {
nextSlots[slotIndex] = nextAttachment;
changed = true;
}
} else if (currentIsPreviousAutoSelection) {
nextSlots[slotIndex] = null;
changed = true;
}
});
if (!changed) {
lastSyncedDetailProductSlotKeysRef.current = nextDetailProductSourceKeys;
lastSyncedDetailBundleSlotKeysRef.current = nextDetailBundleSourceKeys;
lastSyncedDetailModelSlotKeyRef.current = nextDetailModelSourceKey;
return current;
}
lastSyncedDetailProductSlotKeysRef.current = nextDetailProductSourceKeys;
lastSyncedDetailBundleSlotKeysRef.current = nextDetailBundleSourceKeys;
lastSyncedDetailModelSlotKeyRef.current = nextDetailModelSourceKey;
return {
...current,
brief: nextBrief,
@ -2626,7 +2729,9 @@ export function AgentGenerationWorkspaceDarkV2({
};
});
}, [
detailGenerationProductSlotIndexes,
isDetailReplacementPreset,
modelSlotEnabled,
referenceImages,
selectedModelAttachment,
selectedProductAttachments,
@ -2734,14 +2839,14 @@ export function AgentGenerationWorkspaceDarkV2({
return;
}
const productAttachments = AGENT_DETAIL_PRODUCT_SLOT_INDEXES.map((index) =>
const productAttachments = detailGenerationProductSlotIndexes.map((index) =>
detailGenerationSlots[index]
).filter((item): item is NonNullable<(typeof detailGenerationSlots)[number]> => Boolean(item?.url));
const bundleAttachments = AGENT_DETAIL_BUNDLE_SLOT_INDEXES.map((index) =>
detailGenerationSlots[index]
).filter((item): item is NonNullable<(typeof detailGenerationSlots)[number]> => Boolean(item?.url));
const modelAttachment =
AGENT_DETAIL_MODEL_SLOT_INDEX >= 0
modelSlotEnabled && AGENT_DETAIL_MODEL_SLOT_INDEX >= 0
? detailGenerationSlots[AGENT_DETAIL_MODEL_SLOT_INDEX]
: null;
@ -2760,9 +2865,10 @@ export function AgentGenerationWorkspaceDarkV2({
: "";
const detailPromptWithWorkflow = `${detailPrompt}${workflowHint}`.trim();
const detailCount = extractAgentDetailFrameCount(detailPromptWithWorkflow);
const referenceImageNotes = AGENT_DETAIL_SLOT_CONFIGS.map((config, index) => {
const current = detailGenerationSlots[index];
return `${index + 1}. ${config.label}${current?.name ? `(${current.name})` : ""}`;
const referenceImageNotes = detailGenerationVisibleSlotIndexes.map((slotIndex, visibleIndex) => {
const config = AGENT_DETAIL_SLOT_CONFIGS[slotIndex];
const current = detailGenerationSlots[slotIndex];
return `${visibleIndex + 1}. ${config?.label || `素材${visibleIndex + 1}`}${current?.name ? `(${current.name})` : ""}`;
});
const outputLanguage: "zh" | "en" = /[a-zA-Z]/.test(detailPromptWithWorkflow)
? "en"
@ -2802,6 +2908,7 @@ export function AgentGenerationWorkspaceDarkV2({
count: detailCount,
generationMode: "classic",
referenceImages: detailGenerationSlots
.filter((_, slotIndex) => detailGenerationVisibleSlotIndexes.includes(slotIndex))
.map((slot) => slot?.url || "")
.filter(Boolean)
.slice(0, 12),
@ -3429,12 +3536,16 @@ export function AgentGenerationWorkspaceDarkV2({
<div className="mb-3 flex items-center justify-between">
<div className="text-xs font-medium text-white/82">素材中心</div>
<div className="text-[11px] text-white/45">
产品图 {detailGenerationProductCount}/5 · 模特图{" "}
{detailGenerationModelReady ? "1/1" : "0/1"} · 搭配图 {detailGenerationBundleCount}/3
产品图 {detailGenerationProductCount}/{detailGenerationRequiredProductCount}
{modelSlotEnabled
? ` · 模特图 ${detailGenerationModelReady ? "1/1" : "0/1"}`
: ""}
{` · 搭配图 ${detailGenerationBundleCount}/3`}
</div>
</div>
<div className="grid grid-cols-2 gap-3 xl:grid-cols-3">
{AGENT_DETAIL_SLOT_CONFIGS.map((slot, index) => {
{detailGenerationVisibleSlotIndexes.map((index) => {
const slot = AGENT_DETAIL_SLOT_CONFIGS[index];
const attachment = detailGenerationSlots[index];
return (
<div