diff --git a/src/components/composer/GenerationComposer.tsx b/src/components/composer/GenerationComposer.tsx index 89789d50..7da788f1 100644 --- a/src/components/composer/GenerationComposer.tsx +++ b/src/components/composer/GenerationComposer.tsx @@ -949,6 +949,7 @@ function ComposerEditor({ useEffect(() => { if (isHidden) { + persistCurrentComposerConfig(); stopReferenceAudioPreview(); onHiddenPersisted?.(); return; diff --git a/src/store/workflowStore.ts b/src/store/workflowStore.ts index 59b989f5..6930a3a6 100644 --- a/src/store/workflowStore.ts +++ b/src/store/workflowStore.ts @@ -641,14 +641,22 @@ function buildReferenceSubjectNodePatch( node: WorkflowNode, beforeMaterials: ComposerReferenceMaterial[], afterMaterials: ComposerReferenceMaterial[], - options: { syncPrompt?: boolean } = {} + options: { + syncPrompt?: boolean; + promptBeforeMaterials?: ComposerReferenceMaterial[]; + promptAfterMaterials?: ComposerReferenceMaterial[]; + } = {} ): Partial | null { const data = node.data as { parameters?: Record; inputPrompt?: unknown; prompt?: unknown }; const previousSubjects = readNodeReferenceSubjects(node); const nextSubjects = buildReferenceSubjectListParameter(afterMaterials); const previousPrompt = readNodeInputPrompt(node); const nextPrompt = options.syncPrompt - ? syncPromptReferenceMaterials(previousPrompt, beforeMaterials, afterMaterials) + ? syncPromptReferenceMaterials( + previousPrompt, + options.promptBeforeMaterials ?? beforeMaterials, + options.promptAfterMaterials ?? afterMaterials + ) : previousPrompt; const nextSubType = isVideoGenerationNode(node) ? normalizeVideoGenerationSubType( @@ -803,11 +811,48 @@ function applyReferenceSubjectListPatch( const orderedAfterMaterials = options.preserveMaterialOrder ? afterMaterials : orderReferenceMaterials(afterMaterials, previousOrder); - const patch = buildReferenceSubjectNodePatch(node, orderedBeforeMaterials, orderedAfterMaterials, options); + const patch = buildReferenceSubjectNodePatch(node, orderedBeforeMaterials, orderedAfterMaterials, { + ...options, + promptBeforeMaterials: beforeMaterials, + promptAfterMaterials: afterMaterials, + }); if (!patch) return; get().updateNodeData(nodeId, patch); } +function collectReferenceMaterialsBeforeRemovingSourceNodes( + removedNodeIds: Set, + get: () => WorkflowStore +): { beforeMaterialsByTarget: Map; removedEdges: WorkflowEdge[] } { + const removedEdges = get().edges.filter( + (edge) => removedNodeIds.has(edge.source) || removedNodeIds.has(edge.target) + ); + const beforeMaterialsByTarget = new Map(); + + removedEdges.forEach((edge) => { + if (!removedNodeIds.has(edge.source) || removedNodeIds.has(edge.target)) return; + if (beforeMaterialsByTarget.has(edge.target)) return; + beforeMaterialsByTarget.set(edge.target, buildNodeReferenceMaterials(edge.target, get)); + }); + + return { beforeMaterialsByTarget, removedEdges }; +} + +function applyReferenceMaterialChangesAfterRemovingSourceNodes( + beforeMaterialsByTarget: Map, + get: () => WorkflowStore +): void { + beforeMaterialsByTarget.forEach((beforeMaterials, targetNodeId) => { + if (!get().nodes.some((node) => node.id === targetNodeId)) return; + get().applyNodeReferenceMaterialsChange( + targetNodeId, + beforeMaterials, + buildNodeReferenceMaterials(targetNodeId, get), + { syncPrompt: true } + ); + }); +} + /** Capture current undoable state as a deep-cloned snapshot */ function captureUndoSnapshot(state: WorkflowStore): UndoSnapshot { const cloned = clonePreservingStrings({ @@ -1490,6 +1535,10 @@ const workflowStoreImpl: StateCreator = (set, get) => ({ removeNode: (nodeId: string) => { pushUndoCheckpoint(get, set); + const { beforeMaterialsByTarget, removedEdges } = collectReferenceMaterialsBeforeRemovingSourceNodes( + new Set([nodeId]), + get + ); set((state) => ({ nodes: state.nodes.filter((node) => node.id !== nodeId), edges: state.edges.filter( @@ -1497,6 +1546,15 @@ const workflowStoreImpl: StateCreator = (set, get) => ({ ), hasUnsavedChanges: true, })); + if (removedEdges.length > 0) { + deleteCheckpointActive = true; + try { + clearStaleInputImages(removedEdges, get); + applyReferenceMaterialChangesAfterRemovingSourceNodes(beforeMaterialsByTarget, get); + } finally { + deleteCheckpointActive = false; + } + } get().incrementManualChangeCount(); }, @@ -1562,6 +1620,13 @@ const workflowStoreImpl: StateCreator = (set, get) => ({ setTimeout(() => { deleteCheckpointActive = false; }, 0); } + const removedNodeIds = new Set( + effectiveChanges.filter((change) => change.type === "remove").map((change) => change.id) + ); + const { beforeMaterialsByTarget, removedEdges } = hasRemoveChange + ? collectReferenceMaterialsBeforeRemovingSourceNodes(removedNodeIds, get) + : { beforeMaterialsByTarget: new Map(), removedEdges: [] }; + set((state) => { const removedGroupIds = new Set(); for (const change of effectiveChanges) { @@ -1647,6 +1712,8 @@ const workflowStoreImpl: StateCreator = (set, get) => ({ }); if (hasRemoveChange) { + clearStaleInputImages(removedEdges, get); + applyReferenceMaterialChangesAfterRemovingSourceNodes(beforeMaterialsByTarget, get); get().incrementManualChangeCount(); } },