From cced65c08341a06ed60649eec522e27a3b93261c Mon Sep 17 00:00:00 2001 From: Tanish Manku Date: Tue, 5 May 2026 13:13:16 -0700 Subject: [PATCH] artcraft: dismissable pending tiles in PageDraw history stack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pending tiles now have a hover-revealed trash button that opens a confirm modal noting the image will still appear in the library when complete. Removing the entry from pendingGenerations is enough — the Tauri completion bridge filters incoming events on membership, so the eventual result is silently dropped from PageDraw. The confirm modal auto-closes if the underlying generation resolves before the user confirms, and clears its tracked target if dismissed externally. --- .../pagedraw/src/lib/HistoryStack.tsx | 78 ++++++++++++++++++- .../components/pagedraw/src/lib/PageDraw.tsx | 3 + .../pagedraw/src/lib/stores/SceneState.ts | 10 +++ 3 files changed, 89 insertions(+), 2 deletions(-) diff --git a/frontend/libs/components/pagedraw/src/lib/HistoryStack.tsx b/frontend/libs/components/pagedraw/src/lib/HistoryStack.tsx index 1b1a97df78..541c7c2575 100644 --- a/frontend/libs/components/pagedraw/src/lib/HistoryStack.tsx +++ b/frontend/libs/components/pagedraw/src/lib/HistoryStack.tsx @@ -5,7 +5,7 @@ import { faTrashXmark, faXmark, } from "@fortawesome/pro-solid-svg-icons"; -import { Fragment, useEffect, useRef } from "react"; +import { Fragment, useEffect, useRef, useState } from "react"; import { twMerge } from "tailwind-merge"; import { BaseSelectorImage, ImageBundle } from "./types"; import { Tooltip } from "@storyteller/ui-tooltip"; @@ -21,6 +21,7 @@ interface HistoryStackProps { onClear: () => void; onImageSelect?: (image: BaseSelectorImage) => void; onImageRemove?: (image: BaseSelectorImage) => void; + onPendingRemove?: (id: string) => void; imageBundles: ImageBundle[]; pendingPlaceholders?: { id: string; count: number }[]; selectedImageToken?: string; @@ -31,6 +32,7 @@ export const HistoryStack = ({ onClear, onImageSelect = () => {}, onImageRemove = () => {}, + onPendingRemove = () => {}, imageBundles, pendingPlaceholders = [], selectedImageToken, @@ -69,6 +71,66 @@ export const HistoryStack = ({ onImageRemove(baseImage); }; + // Tracks which pending tile (if any) currently has its remove-confirmation + // modal open, so we can auto-close the modal if the underlying generation + // finishes loading before the user confirms. + const [pendingRemoveTargetId, setPendingRemoveTargetId] = useState< + string | null + >(null); + + const handlePendingRemoveClick = (id: string) => { + setPendingRemoveTargetId(id); + showActionReminder({ + reminderType: "default", + title: "Remove pending generation", + primaryActionIcon: faTrashXmark, + primaryActionBtnClassName: "bg-red hover:bg-red/80", + message: ( + <> +

+ Stop showing this generation in the history stack? This action + cannot be undone. +

+

+ The image may still finish in the background and will be available + in your library when it does. +

+ + ), + primaryActionText: "Remove", + onPrimaryAction: () => { + onPendingRemove(id); + setPendingRemoveTargetId(null); + isActionReminderOpen.value = false; + }, + }); + }; + + // If the pending generation we opened the confirm modal for resolves + // before the user clicks confirm, close the modal — there's nothing left + // to dismiss. + useEffect(() => { + if (pendingRemoveTargetId === null) return; + const stillPending = pendingPlaceholders.some( + (p) => p.id === pendingRemoveTargetId, + ); + if (!stillPending) { + isActionReminderOpen.value = false; + setPendingRemoveTargetId(null); + } + }, [pendingPlaceholders, pendingRemoveTargetId]); + + // If the modal is dismissed by other means (Escape, backdrop click), clear + // our target so we don't try to manage a modal that's already gone. + // Uses the signal's subscribe API so we react to external dismissals. + useEffect(() => { + if (pendingRemoveTargetId === null) return; + const unsubscribe = isActionReminderOpen.subscribe((isOpen) => { + if (!isOpen) setPendingRemoveTargetId(null); + }); + return unsubscribe; + }, [pendingRemoveTargetId]); + // Scroll to top when new pending placeholders are added (after enqueue) useEffect(() => { const current = pendingPlaceholders.length; @@ -126,7 +188,7 @@ export const HistoryStack = ({ (_, i) => (
{blurredBackgroundUrl && ( @@ -163,6 +225,18 @@ export const HistoryStack = ({ />
+
{ + e.stopPropagation(); + handlePendingRemoveClick(p.id); + }} + > + +
), )} diff --git a/frontend/libs/components/pagedraw/src/lib/PageDraw.tsx b/frontend/libs/components/pagedraw/src/lib/PageDraw.tsx index b49b28a217..b733df5cfe 100644 --- a/frontend/libs/components/pagedraw/src/lib/PageDraw.tsx +++ b/frontend/libs/components/pagedraw/src/lib/PageDraw.tsx @@ -291,6 +291,7 @@ const PageDraw = ({ adapter }: PageDrawProps) => { addHistoryImageBundle: state.addHistoryImageBundle, addPendingGeneration: state.addPendingGeneration, resolvePendingGeneration: state.resolvePendingGeneration, + removePendingGeneration: state.removePendingGeneration, clearPendingGenerations: state.clearPendingGenerations, setAspectRatioType: state.setAspectRatioType, setActiveTool: state.setActiveTool, @@ -344,6 +345,7 @@ const PageDraw = ({ adapter }: PageDrawProps) => { addHistoryImageBundle, addPendingGeneration, resolvePendingGeneration, + removePendingGeneration, clearPendingGenerations, setAspectRatioType, setActiveTool, @@ -988,6 +990,7 @@ const PageDraw = ({ adapter }: PageDrawProps) => { removeHistoryImage(baseImage); } }} + onPendingRemove={removePendingGeneration} selectedImageToken={baseImageInfo?.mediaToken} /> diff --git a/frontend/libs/components/pagedraw/src/lib/stores/SceneState.ts b/frontend/libs/components/pagedraw/src/lib/stores/SceneState.ts index bb1273354d..1012e92f53 100644 --- a/frontend/libs/components/pagedraw/src/lib/stores/SceneState.ts +++ b/frontend/libs/components/pagedraw/src/lib/stores/SceneState.ts @@ -285,6 +285,10 @@ export interface SceneState { pendingGenerations: { id: string; count: number }[]; addPendingGeneration: (id: string, count: number) => void; resolvePendingGeneration: (id: string) => void; + // User-initiated dismissal of a still-loading placeholder. Removing the + // entry from pendingGenerations is enough to make the Tauri completion + // bridge ignore the eventual result (its filter checks membership). + removePendingGeneration: (id: string) => void; clearPendingGenerations: () => void; toggleLock: (nodeIds: string[]) => void; @@ -1503,6 +1507,12 @@ export const useSceneStore = create((set, get, store) => ({ })); }, + removePendingGeneration: (id: string) => { + set((state) => ({ + pendingGenerations: state.pendingGenerations.filter((p) => p.id !== id), + })); + }, + clearPendingGenerations: () => { set({ pendingGenerations: [] }); },