fix: persist PSD editor uploads and results
This commit is contained in:
parent
ded97bef00
commit
aa885a3d48
@ -1046,6 +1046,8 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
const [isPsdTemplateDialogOpen, setIsPsdTemplateDialogOpen] = useState(false);
|
||||
const [psdTemplateDialogSourceUrl, setPsdTemplateDialogSourceUrl] = useState<string | null>(null);
|
||||
const [psdTemplateDialogMode, setPsdTemplateDialogMode] = useState<AgentPsdTemplateMode>("main");
|
||||
const [psdTemplateDialogResultModeType, setPsdTemplateDialogResultModeType] =
|
||||
useState<AgentGenerationResultModeType>("scene_generation");
|
||||
const [batchDownloadState, setBatchDownloadState] = useState<null | "scene" | "detail" | "sku">(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
|
||||
}
|
||||
/>
|
||||
</ClientErrorBoundary>
|
||||
|
||||
|
||||
@ -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<string>();
|
||||
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<TLayer extends AgentPsdTemplateLayerSlot>(params: {
|
||||
nextLayer: TLayer;
|
||||
previousLayers: TLayer[];
|
||||
usedPreviousLayerIds?: Set<string>;
|
||||
}) {
|
||||
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<string, string>;
|
||||
previousImagePlacements: Record<string, AgentPsdImagePlacement>;
|
||||
previousTextAssignments: Record<string, string>;
|
||||
previousTextColorAssignments: Record<string, string>;
|
||||
}) {
|
||||
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<string, string>,
|
||||
};
|
||||
|
||||
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<string>();
|
||||
const usedNextImageLayerIds = new Set<string>();
|
||||
|
||||
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<string>();
|
||||
|
||||
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<HTMLDivElement | null>(null);
|
||||
const uploadInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const inlineTextEditorRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const previousSelectedTemplateRef = useRef<AgentPsdTemplate | null>(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({
|
||||
<DialogContent
|
||||
className="flex h-[96vh] w-[96vw] max-w-[1700px] flex-col overflow-hidden rounded-[24px] border border-white/12 bg-[#0a0a0a] p-0 text-white shadow-[0_40px_100px_rgba(0,0,0,0.8)]"
|
||||
hideCloseButton
|
||||
onEscapeKeyDown={(event) => {
|
||||
event.preventDefault();
|
||||
}}
|
||||
onInteractOutside={(event) => {
|
||||
event.preventDefault();
|
||||
}}
|
||||
onPointerDownOutside={(event) => {
|
||||
event.preventDefault();
|
||||
}}
|
||||
overlayClassName="bg-black/80 backdrop-blur-md"
|
||||
>
|
||||
{fontFaceCss ? <style>{fontFaceCss}</style> : null}
|
||||
@ -2032,19 +2316,6 @@ export function AgentPsdTemplateEditorDialog({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="h-12 w-full rounded-xl bg-gradient-to-r from-emerald-500 to-teal-400 font-medium text-white shadow-[0_4px_20px_rgba(16,185,129,0.25)] ring-1 ring-emerald-500/50 transition-all hover:from-emerald-400 hover:to-teal-300 hover:shadow-[0_6px_25px_rgba(16,185,129,0.35)] disabled:opacity-50"
|
||||
disabled={isRendering}
|
||||
onClick={handleRender}
|
||||
>
|
||||
{isRendering ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Sparkles className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
应用模板并导出
|
||||
</Button>
|
||||
|
||||
{sourceImageUrl ? null : (
|
||||
<div className="rounded-xl border border-white/10 border-dashed bg-white/[0.02] px-4 py-3 text-white/50 text-xs leading-relaxed shadow-inner">
|
||||
当前未从生图结果带入图片。请先在下方图片图层中点击“上传替换图片”,再开始套用
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user