artcraft: dismissable pending tiles in PageDraw history stack

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.
This commit is contained in:
Tanish Manku authored and Brandon Thomas committed 2026-05-08 01:15:45 -04:00
1 parent bf0f8ee3b2
commit cced65c083
3 files changed
+89 -2

No files matched your search

@@ -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: (
<>
<p className="text-base-fg text-sm opacity-70">
Stop showing this generation in the history stack? This action
cannot be undone.
</p>
<p className="text-base-fg text-sm opacity-70">
The image may still finish in the background and will be available
in your library when it does.
</p>
</>
),
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) => (
<div
key={`pending-${p.id}-${i}`}
className="relative w-full"
className="group relative w-full"
>
<div className="st-loading-tile relative aspect-square w-full overflow-hidden rounded-lg">
{blurredBackgroundUrl && (
@@ -163,6 +225,18 @@ export const HistoryStack = ({
/>
</svg>
</div>
<div
className="absolute -right-0 -top-0 z-10 flex h-5 w-5 cursor-pointer items-center justify-center rounded-bl-lg bg-red/50 opacity-0 transition-opacity hover:bg-red/80 group-hover:opacity-100"
onClick={(e) => {
e.stopPropagation();
handlePendingRemoveClick(p.id);
}}
>
<FontAwesomeIcon
icon={faTrashAlt}
className="text-base-fg h-full w-full text-[13px]"
/>
</div>
</div>
),
)}
@@ -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}
/>
</div>
@@ -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<SceneState>((set, get, store) => ({
}));
},
removePendingGeneration: (id: string) => {
set((state) => ({
pendingGenerations: state.pendingGenerations.filter((p) => p.id !== id),
}));
},
clearPendingGenerations: () => {
set({ pendingGenerations: [] });
},