From f97e96c5f9b68185c65ebb2f3078a2608aef8d7c Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 25 Mar 2026 15:48:21 +0800 Subject: [PATCH] fix: sync ai detail material slots with workflow --- .../agents/AgentGenerationWorkspaceDarkV2.tsx | 187 ++++++++++++++---- 1 file changed, 149 insertions(+), 38 deletions(-) diff --git a/components/agents/AgentGenerationWorkspaceDarkV2.tsx b/components/agents/AgentGenerationWorkspaceDarkV2.tsx index 72e9e6073..b63016276 100644 --- a/components/agents/AgentGenerationWorkspaceDarkV2.tsx +++ b/components/agents/AgentGenerationWorkspaceDarkV2.tsx @@ -859,6 +859,9 @@ export function AgentGenerationWorkspaceDarkV2({ const generatedImageKeysRef = useRef>(new Set()); const lastSyncedBrandModelUrlRef = useRef(null); const lastSyncedProductSlotKeysRef = useRef>([]); + const lastSyncedDetailProductSlotKeysRef = useRef>([]); + const lastSyncedDetailBundleSlotKeysRef = useRef>([]); + const lastSyncedDetailModelSlotKeyRef = useRef(null); const detailGenerationCancelRef = useRef(false); const workspaceStateSignatureRef = useRef(""); const [localAgentModeState, setLocalAgentModeState] = useState( @@ -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({
素材中心
- 产品图 {detailGenerationProductCount}/5 · 模特图{" "} - {detailGenerationModelReady ? "1/1" : "0/1"} · 搭配图 {detailGenerationBundleCount}/3 + 产品图 {detailGenerationProductCount}/{detailGenerationRequiredProductCount} + {modelSlotEnabled + ? ` · 模特图 ${detailGenerationModelReady ? "1/1" : "0/1"}` + : ""} + {` · 搭配图 ${detailGenerationBundleCount}/3`}
- {AGENT_DETAIL_SLOT_CONFIGS.map((slot, index) => { + {detailGenerationVisibleSlotIndexes.map((index) => { + const slot = AGENT_DETAIL_SLOT_CONFIGS[index]; const attachment = detailGenerationSlots[index]; return (