diff --git a/public/scripts/action-loader.js b/public/scripts/action-loader.js index f8c427e47..cc5575edb 100644 --- a/public/scripts/action-loader.js +++ b/public/scripts/action-loader.js @@ -73,6 +73,15 @@ function hasBlockingLoaders() { * Manages its own toast, stop handler, and lifecycle. */ export class ActionLoaderHandle { + /** + * A special empty handle that is already disposed. Useful as a default value to avoid null checks. + * Does not generate any id, toast, or overlay, and all its methods are no-ops. + * @type {ActionLoaderHandle} + */ + static get EMPTY() { + return new ActionLoaderHandle({ predisposed: true }); + } + /** @type {string} Unique identifier for this handle */ id; @@ -99,6 +108,7 @@ export class ActionLoaderHandle { * @param {string} [options.message='Generating...'] - Message to display in the toast * @param {string} [options.title] - Title for the toast notification * @param {string} [options.stopTooltip='Stop'] - Tooltip for the stop button + * @param {boolean} [options.predisposed=false] - Whether this handle is already disposed (for special use) * @param {HTMLElement|string|null} [options.overlayContent] - Custom content for the overlay (replaces default spinner) * @param {(() => void)|null} [options.onStop] - Custom stop handler * @param {(() => void)|null} [options.onHide] - Custom hide handler @@ -112,7 +122,13 @@ export class ActionLoaderHandle { overlayContent = null, onStop = null, onHide = null, + predisposed = false, } = {}) { + if (predisposed) { + this.#disposed = true; + return; + } + this.id = generateLoaderId(); this.#blocking = blocking; this.#onStop = onStop; diff --git a/public/scripts/extensions/stable-diffusion/button.html b/public/scripts/extensions/stable-diffusion/button.html index 2962ff4f4..c16cb15b7 100644 --- a/public/scripts/extensions/stable-diffusion/button.html +++ b/public/scripts/extensions/stable-diffusion/button.html @@ -2,7 +2,3 @@
Generate Image -
-
- Stop Image Generation -
diff --git a/public/scripts/extensions/stable-diffusion/index.js b/public/scripts/extensions/stable-diffusion/index.js index f8d00213c..6cae4a058 100644 --- a/public/scripts/extensions/stable-diffusion/index.js +++ b/public/scripts/extensions/stable-diffusion/index.js @@ -62,18 +62,13 @@ import { t, translate } from '../../i18n.js'; import { oai_settings } from '../../openai.js'; import { power_user } from '/scripts/power-user.js'; import { MacrosParser } from '/scripts/macros.js'; +import { ActionLoaderHandle, loader } from '/scripts/action-loader.js'; export { MODULE_NAME }; const MODULE_NAME = 'sd'; // This is a 1x1 transparent PNG const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='; -const CUSTOM_STOP_EVENT = 'sd_stop_generation'; - -// Generation tracking for status indicator -let activeGenerations = 0; -/** @type {JQuery|null} */ -let generationToast = null; const sources = { extras: 'extras', @@ -2980,62 +2975,6 @@ function ensureSelectionExists(setting, selector) { } } -/** - * Updates the generation status indicator based on active generation count. - * Shows/hides various UI indicators to inform user of background image generation. - */ -function updateGenerationIndicator() { - if (activeGenerations > 0) { - const countText = activeGenerations > 1 ? ` (${activeGenerations})` : ''; - const toastText = ` ${t`Generating an image`}${countText}...`; - - // Show persistent toast if not already showing - if (!generationToast) { - generationToast = toastr.info( - toastText, - 'Image Generation', - { - timeOut: 0, - extendedTimeOut: 0, - tapToDismiss: true, - escapeHtml: false, - onHidden: () => { - generationToast = null; - }, - }, - ); - } else if (activeGenerations > 1) { - // Update count in existing toast - const toastMessage = $(generationToast).find('.toast-message'); - if (toastMessage.length) { - toastMessage.html(toastText); - } - } - } else { - // Hide toast when done - if (generationToast) { - toastr.clear(generationToast); - generationToast = null; - } - } -} - -/** - * Increments the active generation counter and updates indicators. - */ -function startGenerationTracking() { - activeGenerations++; - updateGenerationIndicator(); -} - -/** - * Decrements the active generation counter and updates indicators. - */ -function endGenerationTracking() { - activeGenerations = Math.max(0, activeGenerations - 1); - updateGenerationIndicator(); -} - /** * Generates an image based on the given trigger word. * @param {string} initiator The initiator of the image generation @@ -3096,12 +3035,13 @@ async function generatePicture(initiator, args, trigger, message, callback) { const dimensions = setTypeSpecificDimensions(generationType); const abortController = new AbortController(); - const stopButton = document.getElementById('sd_stop_gen'); let negativePromptPrefix = args?.negative || ''; let imagePath = ''; const stopListener = () => abortController.abort('Aborted by user'); + let loaderHandle = ActionLoaderHandle.EMPTY; + try { const combineNegatives = (prefix) => { negativePromptPrefix = combinePrefixes(negativePromptPrefix, prefix); }; @@ -3114,16 +3054,18 @@ async function generatePicture(initiator, args, trigger, message, callback) { await eventSource.emit(event_types.SD_PROMPT_PROCESSING, eventData); prompt = eventData.prompt; // Allow extensions to modify the prompt - // Track this generation for status indicator - startGenerationTracking(); - // Show stop button after prompt is ready (prompt generation uses separate abort mechanism) - $(stopButton).show(); - eventSource.once(CUSTOM_STOP_EVENT, stopListener); - if (typeof args?._abortController?.addEventListener === 'function') { args._abortController.addEventListener('abort', stopListener); } + // Show non-blocking stoppable toast for this generation + loaderHandle = loader.show({ + blocking: false, + title: t`Image Generation`, + message: t`Generating an image...`, + onStop: stopListener, + }); + // generate the image imagePath = await sendGenerationRequest(generationType, prompt, negativePromptPrefix, characterName, callback, initiator, abortController.signal); } catch (err) { @@ -3142,10 +3084,8 @@ async function generatePicture(initiator, args, trigger, message, callback) { toastr.error(errorText, 'Image Generation'); throw new Error(errorText); } finally { - $(stopButton).hide(); restoreOriginalDimensions(dimensions); - eventSource.removeListener(CUSTOM_STOP_EVENT, stopListener); - endGenerationTracking(); + await loaderHandle.hide(); } return imagePath; @@ -5128,10 +5068,6 @@ async function addSDGenButtons() { generatePicture(initiators.wand, {}, param); } }); - - const stopGenButton = $('#sd_stop_gen'); - stopGenButton.hide(); - stopGenButton.on('click', () => eventSource.emit(CUSTOM_STOP_EVENT)); } function isValidState() { @@ -5216,10 +5152,6 @@ async function sdMessageButton($icon, { animate } = {}) { $icon.toggleClass(classes.idle, !isBusy); $icon.toggleClass(classes.busy, isBusy); $media.toggleClass(classes.animation, isBusy); - - // Update generation counter toast - const trackingFunction = isBusy ? startGenerationTracking : endGenerationTracking; - trackingFunction(); } let $media = jQuery(); @@ -5334,7 +5266,6 @@ async function writePromptFields(characterId) { * @returns {Promise} - A promise that resolves to the newly generated media attachment, or null if generation failed or was aborted. */ async function generateMediaSwipe(mediaAttachment, message, onStart, onComplete, abortController = new AbortController()) { - const stopButton = document.getElementById('sd_stop_gen'); const stopListener = () => abortController.abort('Aborted by user'); const generationType = mediaAttachment.generation_type ?? message?.extra?.generationType ?? generationMode.FREE; let dimensions = { width: extension_settings.sd.width, height: extension_settings.sd.height }; @@ -5348,9 +5279,9 @@ async function generateMediaSwipe(mediaAttachment, message, onStart, onComplete, source: MEDIA_SOURCE.GENERATED, }; + let loaderHandle = ActionLoaderHandle.EMPTY; + try { - $(stopButton).show(); - eventSource.once(CUSTOM_STOP_EVENT, stopListener); const callback = (_a, _b, _c, _d, _e, _f, format) => { result.type = isVideo(format) ? MEDIA_TYPE.VIDEO : MEDIA_TYPE.IMAGE; }; const savedPrompt = mediaAttachment.title ?? message.extra.title ?? ''; const savedNegative = mediaAttachment.negative ?? message.extra.negative ?? ''; @@ -5366,6 +5297,14 @@ async function generateMediaSwipe(mediaAttachment, message, onStart, onComplete, ? context.groups[Object.keys(context.groups).filter(x => context.groups[x].id === context.groupId)[0]]?.id?.toString() : context.characters[context.characterId]?.name; + // Show non-blocking stoppable toast for this generation + loaderHandle = loader.show({ + blocking: false, + title: t`Image Generation`, + message: t`Generating an image...`, + onStop: stopListener, + }); + onStart(); result.url = await sendGenerationRequest(generationType, prompt, refineArgs.negative, characterName, callback, initiators.swipe, abortController.signal); result.generation_type = generationType; @@ -5377,11 +5316,10 @@ async function generateMediaSwipe(mediaAttachment, message, onStart, onComplete, } } finally { onComplete(); - $(stopButton).hide(); - eventSource.removeListener(CUSTOM_STOP_EVENT, stopListener); restoreOriginalDimensions(dimensions); extension_settings.sd.seed = extension_settings.sd.original_seed; delete extension_settings.sd.original_seed; + await loaderHandle.hide(); } if (!result.url) {