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