From aa885a3d48bd35cf58e8e02f8fcf24d1bcc50885 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 10 Apr 2026 20:17:36 +0800 Subject: [PATCH] fix: persist PSD editor uploads and results --- .../agents/AgentGenerationWorkspaceDarkV2.tsx | 50 ++- .../agents/AgentPsdTemplateEditorDialog.tsx | 313 ++++++++++++++++-- 2 files changed, 341 insertions(+), 22 deletions(-) diff --git a/components/agents/AgentGenerationWorkspaceDarkV2.tsx b/components/agents/AgentGenerationWorkspaceDarkV2.tsx index 2acedd3ef..96ff1ad92 100644 --- a/components/agents/AgentGenerationWorkspaceDarkV2.tsx +++ b/components/agents/AgentGenerationWorkspaceDarkV2.tsx @@ -1046,6 +1046,8 @@ export function AgentGenerationWorkspaceDarkV2({ const [isPsdTemplateDialogOpen, setIsPsdTemplateDialogOpen] = useState(false); const [psdTemplateDialogSourceUrl, setPsdTemplateDialogSourceUrl] = useState(null); const [psdTemplateDialogMode, setPsdTemplateDialogMode] = useState("main"); + const [psdTemplateDialogResultModeType, setPsdTemplateDialogResultModeType] = + useState("scene_generation"); const [batchDownloadState, setBatchDownloadState] = useState(null); const [detailReplacementSearchState, setDetailReplacementSearchState] = useState<{ isLoading: boolean; @@ -1078,11 +1080,13 @@ export function AgentGenerationWorkspaceDarkV2({ setIsPsdTemplateDialogOpen(false); setPsdTemplateDialogSourceUrl(null); setPsdTemplateDialogMode("main"); + setPsdTemplateDialogResultModeType("scene_generation"); }, []); const openPsdTemplateDialog = useCallback( (params?: { sourceImageUrl?: string | null; mode?: AgentPsdTemplateMode | null; + resultModeType?: AgentGenerationResultModeType | null; }) => { setPsdTemplateDialogSourceUrl( isValidChatHttpUrl(params?.sourceImageUrl) @@ -1092,9 +1096,14 @@ export function AgentGenerationWorkspaceDarkV2({ setPsdTemplateDialogMode( params?.mode === "sku" ? "sku" : params?.mode === "main" ? "main" : "main" ); + setPsdTemplateDialogResultModeType( + params?.resultModeType || + activeResultModeType || + "scene_generation" + ); setIsPsdTemplateDialogOpen(true); }, - [] + [activeResultModeType] ); const normalizedDeletedResultCollections = useMemo( () => ({ @@ -2952,6 +2961,39 @@ export function AgentGenerationWorkspaceDarkV2({ }, [mergedResultCollections.scene_generation, onResultPreviewUrlsChange, uploadBase64ResultToStorage] ); + const appendResultUrlToCollection = useCallback( + (modeType: AgentGenerationResultModeType, imageUrl: string) => { + const normalizedUrl = String(imageUrl || "").trim(); + if (!normalizedUrl || !onResultPreviewUrlsChange) { + return false; + } + + const nextUrls = Array.from( + new Set([ + ...normalizeResultCollectionUrls(mergedResultCollections[modeType]), + normalizedUrl, + ]) + ); + onResultPreviewUrlsChange(modeType, nextUrls); + if (activeResultModeType === modeType) { + setSelectedResultUrl(normalizedUrl); + } + return true; + }, + [activeResultModeType, mergedResultCollections, onResultPreviewUrlsChange] + ); + const handlePsdDerivedImageCreated = useCallback( + (imageUrl: string) => { + appendResultUrlToCollection(psdTemplateDialogResultModeType, imageUrl); + }, + [appendResultUrlToCollection, psdTemplateDialogResultModeType] + ); + const handlePsdRenderedResultCreated = useCallback( + (imageUrl: string) => { + appendResultUrlToCollection(psdTemplateDialogResultModeType, imageUrl); + }, + [appendResultUrlToCollection, psdTemplateDialogResultModeType] + ); const handleDeleteResultImage = useCallback( (modeType: AgentGenerationResultModeType, imageUrl: string) => { const normalizedUrl = String(imageUrl || "").trim(); @@ -6085,6 +6127,12 @@ export function AgentGenerationWorkspaceDarkV2({ selectedCategoryNames={[workflowContext?.fifthCategory || ""].filter(Boolean)} sizeInfoText={workflowContext?.summaryText || null} templateMode={psdTemplateDialogMode} + onDerivedImageCreated={ + onResultPreviewUrlsChange ? handlePsdDerivedImageCreated : undefined + } + onRenderedResultCreated={ + onResultPreviewUrlsChange ? handlePsdRenderedResultCreated : undefined + } /> diff --git a/components/agents/AgentPsdTemplateEditorDialog.tsx b/components/agents/AgentPsdTemplateEditorDialog.tsx index 3e14756e9..b22fb35c8 100644 --- a/components/agents/AgentPsdTemplateEditorDialog.tsx +++ b/components/agents/AgentPsdTemplateEditorDialog.tsx @@ -429,6 +429,231 @@ function createInitialState( return { imageAssignments, imagePlacements, textAssignments }; } +function normalizeLayerTransferKey(value: string) { + return String(value || "").trim().toLowerCase(); +} + +function getLayerTransferKeys(layer: AgentPsdTemplateLayerSlot) { + const keys = new Set(); + const addKey = (value?: string | null) => { + const normalized = normalizeLayerTransferKey(String(value || "")); + if (normalized) { + keys.add(normalized); + } + }; + + addKey(layer.id); + addKey(layer.layerId); + addKey(layer.layerPath); + addKey(layer.layerName); + + if (isAgentPsdImageLayerSlot(layer)) { + addKey(`image:${layer.layerPath}`); + addKey(`image:${layer.layerName}`); + addKey(`image:${layer.layerPath}:${layer.width}x${layer.height}`); + addKey(`image:${layer.layerName}:${layer.width}x${layer.height}`); + if (layer.isDefaultResultSource) { + addKey("__default_result_source__"); + } + } + + if (isAgentPsdTextLayerSlot(layer)) { + addKey(`text:${layer.layerPath}`); + addKey(`text:${layer.layerName}`); + addKey(`text:${layer.layerPath}:${layer.width}x${layer.height}`); + addKey(`text:${layer.layerName}:${layer.width}x${layer.height}`); + } + + return Array.from(keys); +} + +function findMatchingLayer(params: { + nextLayer: TLayer; + previousLayers: TLayer[]; + usedPreviousLayerIds?: Set; +}) { + const { nextLayer, previousLayers, usedPreviousLayerIds } = params; + const nextLayerKeySet = new Set(getLayerTransferKeys(nextLayer)); + for (const previousLayer of previousLayers) { + if (usedPreviousLayerIds?.has(previousLayer.id)) { + continue; + } + + const previousKeys = getLayerTransferKeys(previousLayer); + if (previousKeys.some((key) => nextLayerKeySet.has(key))) { + return previousLayer; + } + } + + return null; +} + +function resolveTransferredSelectedLayerId(params: { + nextTemplate: AgentPsdTemplate | null; + previousTemplate: AgentPsdTemplate | null; + previousSelectedLayerId: string; +}) { + const { nextTemplate, previousTemplate, previousSelectedLayerId } = params; + if (!nextTemplate) { + return ""; + } + + if (!previousTemplate || !previousSelectedLayerId) { + return getPreferredSelectedLayerId(nextTemplate); + } + + const previousLayer = + previousTemplate.layers.find((layer) => layer.id === previousSelectedLayerId) || + null; + if (!previousLayer) { + return getPreferredSelectedLayerId(nextTemplate); + } + + const nextCandidateLayers = nextTemplate.layers.filter( + (layer) => + layer.isEditable && + ((isAgentPsdImageLayerSlot(previousLayer) && isAgentPsdImageLayerSlot(layer)) || + (isAgentPsdTextLayerSlot(previousLayer) && isAgentPsdTextLayerSlot(layer))) + ); + const previousLayerKeySet = new Set(getLayerTransferKeys(previousLayer)); + const matchedLayer = nextCandidateLayers.find((layer) => + getLayerTransferKeys(layer).some((key) => previousLayerKeySet.has(key)) + ); + + return matchedLayer?.id || getPreferredSelectedLayerId(nextTemplate); +} + +function createTemplateEditorState(params: { + nextTemplate: AgentPsdTemplate | null; + previousTemplate: AgentPsdTemplate | null; + sourceImageUrl: string; + previousImageAssignments: Record; + previousImagePlacements: Record; + previousTextAssignments: Record; + previousTextColorAssignments: Record; +}) { + const { + nextTemplate, + previousTemplate, + sourceImageUrl, + previousImageAssignments, + previousImagePlacements, + previousTextAssignments, + previousTextColorAssignments, + } = params; + + const initialState = createInitialState(nextTemplate, sourceImageUrl); + const nextState = { + imageAssignments: { ...initialState.imageAssignments }, + imagePlacements: { ...initialState.imagePlacements }, + textAssignments: { ...initialState.textAssignments }, + textColorAssignments: {} as Record, + }; + + if (!nextTemplate || !previousTemplate) { + return nextState; + } + + const previousEditableImageLayers = previousTemplate.layers.filter( + (layer): layer is AgentPsdTemplateImageLayerSlot => + layer.isEditable && isAgentPsdImageLayerSlot(layer) + ); + const nextEditableImageLayers = nextTemplate.layers.filter( + (layer): layer is AgentPsdTemplateImageLayerSlot => + layer.isEditable && isAgentPsdImageLayerSlot(layer) + ); + const usedPreviousImageLayerIds = new Set(); + const usedNextImageLayerIds = new Set(); + + for (const nextLayer of nextEditableImageLayers) { + const matchedPreviousLayer = findMatchingLayer({ + nextLayer, + previousLayers: previousEditableImageLayers, + usedPreviousLayerIds: usedPreviousImageLayerIds, + }); + if (!matchedPreviousLayer) { + continue; + } + + const previousImageUrl = String( + previousImageAssignments[matchedPreviousLayer.id] || "" + ).trim(); + if (!previousImageUrl) { + continue; + } + + nextState.imageAssignments[nextLayer.id] = previousImageUrl; + nextState.imagePlacements[nextLayer.id] = normalizeAgentPsdImagePlacement( + previousImagePlacements[matchedPreviousLayer.id] + ); + usedPreviousImageLayerIds.add(matchedPreviousLayer.id); + usedNextImageLayerIds.add(nextLayer.id); + } + + const unmatchedPreviousImageLayers = previousEditableImageLayers.filter( + (layer) => + !usedPreviousImageLayerIds.has(layer.id) && + Boolean(String(previousImageAssignments[layer.id] || "").trim()) + ); + if (unmatchedPreviousImageLayers.length === 1) { + const fallbackNextLayer = + nextEditableImageLayers.find( + (layer) => + !usedNextImageLayerIds.has(layer.id) && layer.isDefaultResultSource + ) || + nextEditableImageLayers.find( + (layer) => !usedNextImageLayerIds.has(layer.id) + ); + if (fallbackNextLayer) { + const fallbackPreviousLayer = unmatchedPreviousImageLayers[0]; + nextState.imageAssignments[fallbackNextLayer.id] = String( + previousImageAssignments[fallbackPreviousLayer.id] || "" + ).trim(); + nextState.imagePlacements[fallbackNextLayer.id] = + normalizeAgentPsdImagePlacement( + previousImagePlacements[fallbackPreviousLayer.id] + ); + } + } + + const previousEditableTextLayers = previousTemplate.layers.filter( + (layer): layer is AgentPsdTemplateTextLayerSlot => + layer.isEditable && isAgentPsdTextLayerSlot(layer) + ); + const nextEditableTextLayers = nextTemplate.layers.filter( + (layer): layer is AgentPsdTemplateTextLayerSlot => + layer.isEditable && isAgentPsdTextLayerSlot(layer) + ); + const usedPreviousTextLayerIds = new Set(); + + for (const nextLayer of nextEditableTextLayers) { + const matchedPreviousLayer = findMatchingLayer({ + nextLayer, + previousLayers: previousEditableTextLayers, + usedPreviousLayerIds: usedPreviousTextLayerIds, + }); + if (!matchedPreviousLayer) { + continue; + } + + const previousTextValue = previousTextAssignments[matchedPreviousLayer.id]; + if (typeof previousTextValue === "string") { + nextState.textAssignments[nextLayer.id] = previousTextValue; + } + + const previousTextColor = String( + previousTextColorAssignments[matchedPreviousLayer.id] || "" + ).trim(); + if (previousTextColor) { + nextState.textColorAssignments[nextLayer.id] = previousTextColor; + } + + usedPreviousTextLayerIds.add(matchedPreviousLayer.id); + } + + return nextState; +} + function getLayerLabel(layer: AgentPsdTemplateLayerSlot) { return isAgentPsdImageLayerSlot(layer) ? "可替换图片" : "可编辑文字"; } @@ -725,6 +950,8 @@ export function AgentPsdTemplateEditorDialog({ selectedCategoryNames, sizeInfoText, templateMode, + onDerivedImageCreated, + onRenderedResultCreated, }: { open: boolean; onOpenChange: (open: boolean) => void; @@ -733,6 +960,8 @@ export function AgentPsdTemplateEditorDialog({ selectedCategoryNames?: string[] | null; sizeInfoText?: string | null; templateMode?: AgentPsdTemplateMode | null; + onDerivedImageCreated?: (imageUrl: string) => void; + onRenderedResultCreated?: (imageUrl: string) => void; }) { const normalizedBrandName = String(brandName || "").trim(); const normalizedSelectedCategoryNames = useMemo( @@ -831,6 +1060,7 @@ export function AgentPsdTemplateEditorDialog({ const previewViewportRef = useRef(null); const uploadInputRef = useRef(null); const inlineTextEditorRef = useRef(null); + const previousSelectedTemplateRef = useRef(null); const imageDragStateRef = useRef<{ layerId: string; pointerId: number; @@ -839,6 +1069,18 @@ export function AgentPsdTemplateEditorDialog({ startPlacement: AgentPsdImagePlacement; } | null>(null); + const imageAssignmentsRef = useRef(imageAssignments); + const imagePlacementsRef = useRef(imagePlacements); + const textAssignmentsRef = useRef(textAssignments); + const textColorAssignmentsRef = useRef(textColorAssignments); + const selectedLayerIdRef = useRef(selectedLayerId); + + imageAssignmentsRef.current = imageAssignments; + imagePlacementsRef.current = imagePlacements; + textAssignmentsRef.current = textAssignments; + textColorAssignmentsRef.current = textColorAssignments; + selectedLayerIdRef.current = selectedLayerId; + const selectedTemplate = useMemo( () => templates.find((template) => template.id === selectedTemplateId) || null, @@ -1269,6 +1511,14 @@ export function AgentPsdTemplateEditorDialog({ setSelectedTemplateId(""); setSelectedLayerId(""); + setImageAssignments({}); + setImagePlacements({}); + setImageSizes({}); + setTextAssignments({}); + setTextColorAssignments({}); + setRenderedResultUrl(""); + setRightPanelView("editor"); + previousSelectedTemplateRef.current = null; }, [open]); useEffect(() => { @@ -1291,15 +1541,26 @@ export function AgentPsdTemplateEditorDialog({ if (!open) return; setRenderedResultUrl(""); setRightPanelView("editor"); - const initialState = createInitialState( - selectedTemplate, - sourceImageUrl || "" - ); + const initialState = createTemplateEditorState({ + nextTemplate: selectedTemplate, + previousTemplate: previousSelectedTemplateRef.current, + sourceImageUrl: sourceImageUrl || "", + previousImageAssignments: imageAssignmentsRef.current, + previousImagePlacements: imagePlacementsRef.current, + previousTextAssignments: textAssignmentsRef.current, + previousTextColorAssignments: textColorAssignmentsRef.current, + }); setImageAssignments(initialState.imageAssignments); setImagePlacements(initialState.imagePlacements); setTextAssignments(initialState.textAssignments); - setTextColorAssignments({}); - setSelectedLayerId(getPreferredSelectedLayerId(selectedTemplate)); + setTextColorAssignments(initialState.textColorAssignments); + setSelectedLayerId( + resolveTransferredSelectedLayerId({ + nextTemplate: selectedTemplate, + previousTemplate: previousSelectedTemplateRef.current, + previousSelectedLayerId: selectedLayerIdRef.current, + }) + ); if (!selectedTemplate || typeof window === "undefined") { return; @@ -1327,6 +1588,10 @@ export function AgentPsdTemplateEditorDialog({ }; }, [open, selectedTemplate?.id]); + useEffect(() => { + previousSelectedTemplateRef.current = selectedTemplate; + }, [selectedTemplate]); + useEffect(() => { if (!selectedTemplate) { setImageSizes((current) => { @@ -1596,7 +1861,12 @@ export function AgentPsdTemplateEditorDialog({ delete next[slotId]; return next; }); - toast.success("图片拓展完成,已替换当前槽位"); + onDerivedImageCreated?.(nextImageUrl); + toast.success( + onDerivedImageCreated + ? "图片拓展完成,已替换当前槽位并同步保存到结果区" + : "图片拓展完成,已替换当前槽位" + ); } catch (error) { toast.error(error instanceof Error ? error.message : "图片拓展失败"); } finally { @@ -1727,7 +1997,12 @@ export function AgentPsdTemplateEditorDialog({ setRenderedResultUrl(payload.result.url); setRightPanelView("export"); - toast.success("模板替换结果已生成"); + onRenderedResultCreated?.(payload.result.url); + toast.success( + onRenderedResultCreated + ? "模板替换结果已生成,并已加入结果区" + : "模板替换结果已生成" + ); } catch (error) { toast.error(error instanceof Error ? error.message : "PSD 模板渲染失败"); } finally { @@ -1787,6 +2062,15 @@ export function AgentPsdTemplateEditorDialog({ { + event.preventDefault(); + }} + onInteractOutside={(event) => { + event.preventDefault(); + }} + onPointerDownOutside={(event) => { + event.preventDefault(); + }} overlayClassName="bg-black/80 backdrop-blur-md" > {fontFaceCss ? : null} @@ -2032,19 +2316,6 @@ export function AgentPsdTemplateEditorDialog({ - - {sourceImageUrl ? null : (
当前未从生图结果带入图片。请先在下方图片图层中点击“上传替换图片”,再开始套用