Add Action Loader Module with Stacking Support and STscript Commands (#5311) * Add action loader utility with stoppable toast notifications * Add slash commands for action loader control (/loader-wrap, /loader-show, /loader-hide, /loader-stop) * Refactor action loader to support stacking multiple loaders with individual toast management - Convert action loader from singleton to class-based handle system (ActionLoaderHandle) - Support multiple concurrent loaders with single overlay and stacked toasts - Add unique ID generation and tracking for each loader instance - Implement onHide callback alongside existing onStop callback - Add getActiveLoaderHandles() and getLoaderHandleById() utility functions - Refactor hideActionLoader() to accept... * Improve action loader overlay and toast handling with better state checks and cleanup timing - Check isLoaderDisplayed() before showing overlay to prevent conflicts with existing loaders - Use toastr.options.hideDuration for toast removal timing instead of hardcoded 250ms - Simplify hideActionLoader() by using getActiveLoaderHandles() helper - Remove redundant empty check in hideActionLoader() - Add clarifying comment for intentional error throw in createClosureHandler() - Remove redundant 'onStop' default * fix stop button toast removal issue by using toastr.clear force option instead of manual removal with timeout * Fix isLoaderDisplayed() by using double negation operator instead of null comparison * Add non-blocking loader support with `blocking` parameter for toast-only action loaders - Add `blocking` option to ActionLoaderHandle (default: true) - Implement hasBlockingLoaders() helper to check for active blocking loaders - Show/hide overlay only when blocking loaders are active - Add `blocking` named argument to /loader-wrap and /loader-show commands - Update help strings with non-blocking usage examples - Import commonEnumProviders for boolean enum and isFalseBoolean utility * Add optional title parameter to action loader toast notifications and reorder constructor parameters for consistency - Add `title` parameter to ActionLoaderOptions and ActionLoaderHandle constructor - Pass title to toastr.info() for toast notifications - Reorder parameters: blocking, toastMode, message, title, stopTooltip (grouped by importance) - Add warning when creating non-blocking loader without toast (invisible to user) - Update /loader-wrap and /loader-show commands with title argument * Add loader utility API to action-loader and expose it in ST context for convenient programmatic access - Create `loader` object with show/hide/active/get methods and ToastMode/Handle exports - Add JSDoc examples for basic usage, non-blocking tasks, and hiding all loaders - Import and expose `loader` in ST context alongside existing loader functions * Split slash command and functional modules * Create abort controller on app init * Remove HTML tags from "returns" declaration --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
Signed| @@ -29,3 +29,27 @@ | |||
| 29 | align-items: center; | 29 | align-items: center; |
| 30 | justify-content: center; | 30 | justify-content: center; |
| 31 | } | 31 | } |
| 32 | |||
| 33 | /* Action loader toast styles */ | ||
| 34 | .action-loader-toast { | ||
| 35 | display: flex; | ||
| 36 | align-items: center; | ||
| 37 | gap: 10px; | ||
| 38 | width: 100%; | ||
| 39 | } | ||
| 40 | |||
| 41 | .action-loader-message { | ||
| 42 | flex: 1; | ||
| 43 | } | ||
| 44 | |||
| 45 | .action-loader-stop { | ||
| 46 | cursor: pointer; | ||
| 47 | font-size: 1.2em; | ||
| 48 | opacity: 0.8; | ||
| 49 | transition: opacity 0.15s ease, color 0.15s ease; | ||
| 50 | } | ||
| 51 | |||
| 52 | .action-loader-stop:hover { | ||
| 53 | opacity: 1; | ||
| 54 | color: color-mix(in srgb, currentColor 40%, #e74c3c 60%); | ||
| 55 | } | ||
| @@ -361,12 +361,36 @@ toastr.options = { | |||
| 361 | // so the toasts still show up inside there. | 361 | // so the toasts still show up inside there. |
| 362 | fixToastrForDialogs(); | 362 | fixToastrForDialogs(); |
| 363 | }, | 363 | }, |
| 364 | onShown: function () { | ||
| 365 | // Set tooltip to the notification message | ||
| 366 | $(this).attr('title', t`Tap to close`); | ||
| 367 | }, | ||
| 368 | }; | 364 | }; |
| 369 | 365 | ||
| 366 | // Run once during startup | ||
| 367 | toastr.subscribe(function (args) { | ||
| 368 | if (args.state !== 'visible') { | ||
| 369 | return; | ||
| 370 | } | ||
| 371 | |||
| 372 | const $container = toastr.getContainer(args.options, false); | ||
| 373 | if (!$container || !$container.length) { | ||
| 374 | return; | ||
| 375 | } | ||
| 376 | |||
| 377 | // toastr has already inserted the element at this point | ||
| 378 | const $toast = args.options.newestOnTop | ||
| 379 | ? $container.children().first() | ||
| 380 | : $container.children().last(); | ||
| 381 | |||
| 382 | // Meaning of "clickable": | ||
| 383 | // Interactable unless tapToDismiss was explicitly false | ||
| 384 | const isInteractable = args.options.tapToDismiss !== false; | ||
| 385 | $toast.toggleClass('interactable', isInteractable); | ||
| 386 | if (isInteractable) { | ||
| 387 | $toast.attr('title', t`Tap to close`); | ||
| 388 | } else { | ||
| 389 | $toast.removeAttr('title'); | ||
| 390 | $toast.addClass('toast-non-interactable'); | ||
| 391 | } | ||
| 392 | }); | ||
| 393 | |||
| 370 | export const characterGroupOverlay = new BulkEditOverlay(); | 394 | export const characterGroupOverlay = new BulkEditOverlay(); |
| 371 | 395 | ||
| 372 | // Markdown converter | 396 | // Markdown converter |
| @@ -600,8 +624,7 @@ export let recentSwipes = 0; | |||
| 600 | export let extension_prompts = {}; | 624 | export let extension_prompts = {}; |
| 601 | 625 | ||
| 602 | export let main_api;// = "kobold"; | 626 | export let main_api;// = "kobold"; |
| 603 | /** @type {AbortController} */ | 627 | let abortController = new AbortController(); |
| 604 | let abortController; | ||
| 605 | 628 | ||
| 606 | //css | 629 | //css |
| 607 | var css_send_form_display = $('<div id=send_form></div>').css('display'); | 630 | var css_send_form_display = $('<div id=send_form></div>').css('display'); |
| @@ -3862,7 +3885,10 @@ export async function generateRawData({ prompt = '', api = null, instructOverrid | |||
| 3862 | 3885 | ||
| 3863 | // Allow extensions to stop generation before it happens | 3886 | // Allow extensions to stop generation before it happens |
| 3864 | const eventAbortController = new AbortController(); | 3887 | const eventAbortController = new AbortController(); |
| 3865 | const abortHook = () => eventAbortController.abort(new Error('Cancelled by extension')); | 3888 | const abortHook = () => { |
| 3889 | abortController.abort(new Error('Cancelled by stop event')); | ||
| 3890 | eventAbortController.abort(new Error('Cancelled by extension')); | ||
| 3891 | }; | ||
| 3866 | eventSource.on(event_types.GENERATION_STOPPED, abortHook); | 3892 | eventSource.on(event_types.GENERATION_STOPPED, abortHook); |
| 3867 | 3893 | ||
| 3868 | try { | 3894 | try { |
| @@ -0,0 +1,355 @@ | |||
| 1 | import { ActionLoaderToastMode, getActiveLoaderHandles, getLoaderHandleById, hideActionLoader, showActionLoader } from './action-loader.js'; | ||
| 2 | import { t } from './i18n.js'; | ||
| 3 | import { SlashCommand } from './slash-commands/SlashCommand.js'; | ||
| 4 | import { SlashCommandNamedArgument, ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js'; | ||
| 5 | import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js'; | ||
| 6 | import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; | ||
| 7 | import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js'; | ||
| 8 | import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; | ||
| 9 | import { isFalseBoolean } from './utils.js'; | ||
| 10 | |||
| 11 | /** | ||
| 12 | * Registers slash commands for the action loader module. | ||
| 13 | */ | ||
| 14 | export function registerActionLoaderSlashCommands() { | ||
| 15 | /** | ||
| 16 | * Helper to create a closure-based handler from a SlashCommandClosure argument. | ||
| 17 | * Allows all possible slash command arg types to be passed in, but only closure is accepted. | ||
| 18 | * @param {string | SlashCommandClosure | (string | SlashCommandClosure)[]} closure - The closure argument | ||
| 19 | * @param {Object} options - Configuration options | ||
| 20 | * @param {string} [options.argName='onStop'] - Name of the argument for error messages | ||
| 21 | * @param {boolean} [options.throwInvalid=true] - Whether to throw an error for invalid input | ||
| 22 | * @returns {(() => Promise<void>)|null} The handler function, or null if no closure | ||
| 23 | */ | ||
| 24 | function createClosureHandler(closure, { argName = 'onStop', throwInvalid = true } = {}) { | ||
| 25 | if (!(closure instanceof SlashCommandClosure)) { | ||
| 26 | if (closure && throwInvalid) { | ||
| 27 | // Throw error on purpose. This is defined as a syntax error. | ||
| 28 | throw new Error(t`Invalid argument for ${argName} provided. This is not a closure.`); | ||
| 29 | } | ||
| 30 | return null; | ||
| 31 | } | ||
| 32 | return async () => { | ||
| 33 | try { | ||
| 34 | const localClosure = closure.getCopy(); | ||
| 35 | localClosure.onProgress = () => { }; | ||
| 36 | await localClosure.execute(); | ||
| 37 | } catch (e) { | ||
| 38 | console.error('Error executing closure handler', e); | ||
| 39 | } | ||
| 40 | }; | ||
| 41 | } | ||
| 42 | |||
| 43 | // Shared loader enum providers | ||
| 44 | const loaderEnumProviders = { | ||
| 45 | toastModeEnumProvider: () => [ | ||
| 46 | new SlashCommandEnumValue(ActionLoaderToastMode.NONE, 'No toast displayed', enumTypes.enum, enumIcons.disabled), | ||
| 47 | new SlashCommandEnumValue(ActionLoaderToastMode.STATIC, 'Static toast without stop button', enumTypes.enum, enumIcons.spinner), | ||
| 48 | new SlashCommandEnumValue(ActionLoaderToastMode.STOPPABLE, 'Toast with stop button (default)', enumTypes.enum, enumIcons.stop), | ||
| 49 | ], | ||
| 50 | loaderHandleProvider: () => getActiveLoaderHandles().map( | ||
| 51 | handle => new SlashCommandEnumValue(handle.id, `Active loader: ${handle.id}`, enumTypes.enum, enumIcons.spinner), | ||
| 52 | ).concat( | ||
| 53 | new SlashCommandEnumValue('Temporary loader handle', 'Any loader handle saved in variables or similar', 'enum', '๐', () => true, () => ''), | ||
| 54 | ), | ||
| 55 | }; | ||
| 56 | |||
| 57 | // /loader-wrap command - wraps a closure with loader display | ||
| 58 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | ||
| 59 | name: 'loader-wrap', | ||
| 60 | returns: 'result of the closure execution', | ||
| 61 | helpString: ` | ||
| 62 | <div> | ||
| 63 | Wraps a closure execution with an action loader overlay and optional toast notification. | ||
| 64 | By default, the loader blocks UI interaction until the closure completes. | ||
| 65 | Multiple loaders can be stacked - each gets its own toast, but the overlay stays single. | ||
| 66 | </div> | ||
| 67 | <div> | ||
| 68 | <strong>Toast modes:</strong> | ||
| 69 | <ul> | ||
| 70 | <li><code>stoppable</code> - Shows toast with a stop button (default)</li> | ||
| 71 | <li><code>static</code> - Shows toast without stop button</li> | ||
| 72 | <li><code>none</code> - No toast, only loader overlay</li> | ||
| 73 | </ul> | ||
| 74 | </div> | ||
| 75 | <div> | ||
| 76 | Set <code>blocking=false</code> to show only a toast without blocking the UI. | ||
| 77 | Useful for background operations like image captioning or generation. | ||
| 78 | </div> | ||
| 79 | <div> | ||
| 80 | The default stop behavior is calling <code>stopGeneration()</code>. | ||
| 81 | If the wrapped action is doing something different than generating, a custom stop closure can be provided. | ||
| 82 | </div> | ||
| 83 | <div> | ||
| 84 | <strong>Examples:</strong> | ||
| 85 | <ul> | ||
| 86 | <li><pre><code class="language-stscript">/loader-wrap message="Generating summary..." {: /gen Summary of the last message | /echo Done :}</code></pre></li> | ||
| 87 | <li><pre><code class="language-stscript">/loader-wrap blocking=false message="Captioning..." {: /caption :}</code></pre></li> | ||
| 88 | <li><pre><code class="language-stscript">/loader-wrap toast=stoppable onStop={: /echo "Stopped by user" :} {: /delay 10000 :}</code></pre></li> | ||
| 89 | </ul> | ||
| 90 | </div> | ||
| 91 | `, | ||
| 92 | namedArgumentList: [ | ||
| 93 | SlashCommandNamedArgument.fromProps({ | ||
| 94 | name: 'blocking', | ||
| 95 | description: 'Whether to show blocking overlay. Set to false for non-blocking toast-only loaders.', | ||
| 96 | typeList: [ARGUMENT_TYPE.BOOLEAN], | ||
| 97 | defaultValue: 'true', | ||
| 98 | enumList: commonEnumProviders.boolean()(), | ||
| 99 | }), | ||
| 100 | SlashCommandNamedArgument.fromProps({ | ||
| 101 | name: 'toast', | ||
| 102 | description: 'Toast display mode: stoppable (with stop button), static (no stop button), or none', | ||
| 103 | typeList: [ARGUMENT_TYPE.STRING], | ||
| 104 | defaultValue: ActionLoaderToastMode.STOPPABLE, | ||
| 105 | enumList: loaderEnumProviders.toastModeEnumProvider(), | ||
| 106 | }), | ||
| 107 | SlashCommandNamedArgument.fromProps({ | ||
| 108 | name: 'message', | ||
| 109 | description: 'Message to display in the toast notification', | ||
| 110 | typeList: [ARGUMENT_TYPE.STRING], | ||
| 111 | defaultValue: 'Generating...', | ||
| 112 | }), | ||
| 113 | SlashCommandNamedArgument.fromProps({ | ||
| 114 | name: 'title', | ||
| 115 | description: 'Optional title for the toast notification', | ||
| 116 | typeList: [ARGUMENT_TYPE.STRING], | ||
| 117 | }), | ||
| 118 | SlashCommandNamedArgument.fromProps({ | ||
| 119 | name: 'stopTooltip', | ||
| 120 | description: 'Tooltip text for the stop button (only used when toast=stoppable)', | ||
| 121 | typeList: [ARGUMENT_TYPE.STRING], | ||
| 122 | defaultValue: 'Stop', | ||
| 123 | }), | ||
| 124 | SlashCommandNamedArgument.fromProps({ | ||
| 125 | name: 'onStop', | ||
| 126 | description: 'Closure to execute when the stop button is clicked. If not provided, uses default stop behavior.', | ||
| 127 | typeList: [ARGUMENT_TYPE.CLOSURE], | ||
| 128 | }), | ||
| 129 | ], | ||
| 130 | unnamedArgumentList: [ | ||
| 131 | SlashCommandArgument.fromProps({ | ||
| 132 | description: 'Closure to execute while the loader is displayed', | ||
| 133 | typeList: [ARGUMENT_TYPE.CLOSURE], | ||
| 134 | isRequired: true, | ||
| 135 | }), | ||
| 136 | ], | ||
| 137 | callback: async (args, value) => { | ||
| 138 | if (!(value instanceof SlashCommandClosure)) { | ||
| 139 | // Throw error on purpose. This is defined as a syntax error. | ||
| 140 | throw new Error(t`Invalid argument for unnamed argument provided. This is not a closure.`); | ||
| 141 | } | ||
| 142 | |||
| 143 | const blocking = !isFalseBoolean(String(args.blocking)); | ||
| 144 | const toastMode = Object.values(ActionLoaderToastMode).includes(String(args.toast)) | ||
| 145 | ? String(args.toast) | ||
| 146 | : ActionLoaderToastMode.STOPPABLE; | ||
| 147 | const message = String(args.message ?? t`Generating...`); | ||
| 148 | const title = args.title ? String(args.title) : ''; | ||
| 149 | const stopTooltip = String(args.stopTooltip ?? t`Stop`); | ||
| 150 | |||
| 151 | const loader = showActionLoader({ | ||
| 152 | blocking, | ||
| 153 | toastMode, | ||
| 154 | message, | ||
| 155 | title, | ||
| 156 | stopTooltip, | ||
| 157 | onStop: createClosureHandler(args.onStop), | ||
| 158 | }); | ||
| 159 | |||
| 160 | try { | ||
| 161 | const closureCopy = value.getCopy(); | ||
| 162 | const result = await closureCopy.execute(); | ||
| 163 | return result.pipe; | ||
| 164 | } finally { | ||
| 165 | await loader.hide(); | ||
| 166 | } | ||
| 167 | }, | ||
| 168 | })); | ||
| 169 | |||
| 170 | // /loader-show command - manually show a loader, returns handle ID | ||
| 171 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | ||
| 172 | name: 'loader-show', | ||
| 173 | returns: 'loader handle ID (use with /loader-hide)', | ||
| 174 | helpString: ` | ||
| 175 | <div> | ||
| 176 | Manually shows an action loader. Returns a handle ID that can be used with <code>/loader-hide</code> to hide it. | ||
| 177 | Use this for fine-grained control when you need to show/hide the loader at specific points. | ||
| 178 | Multiple loaders can be stacked - each gets its own toast, but the overlay stays single. | ||
| 179 | </div> | ||
| 180 | <div> | ||
| 181 | <strong>Toast modes:</strong> | ||
| 182 | <ul> | ||
| 183 | <li><code>stoppable</code> - Shows toast with a stop button (default)</li> | ||
| 184 | <li><code>static</code> - Shows toast without stop button</li> | ||
| 185 | <li><code>none</code> - No toast, only loader overlay</li> | ||
| 186 | </ul> | ||
| 187 | </div> | ||
| 188 | <div> | ||
| 189 | Set <code>blocking=false</code> to show only a toast without blocking the UI. | ||
| 190 | Useful for background operations like image captioning or generation. | ||
| 191 | </div> | ||
| 192 | <div> | ||
| 193 | The default stop behavior is calling <code>stopGeneration()</code>. | ||
| 194 | If the wrapped action is doing something different than generating, a custom stop closure can be provided. | ||
| 195 | </div> | ||
| 196 | <div> | ||
| 197 | <strong>Example:</strong> | ||
| 198 | <pre> | ||
| 199 | <code class="language-stscript"> | ||
| 200 | /loader-show message="Loading..." | | ||
| 201 | /setvar key=myLoader | | ||
| 202 | /some-operation | | ||
| 203 | /loader-hide handle={{getvar::myLoader}} | ||
| 204 | </code> | ||
| 205 | </pre> | ||
| 206 | </div> | ||
| 207 | `, | ||
| 208 | namedArgumentList: [ | ||
| 209 | SlashCommandNamedArgument.fromProps({ | ||
| 210 | name: 'blocking', | ||
| 211 | description: 'Whether to show blocking overlay. Set to false for non-blocking toast-only loaders.', | ||
| 212 | typeList: [ARGUMENT_TYPE.BOOLEAN], | ||
| 213 | defaultValue: 'true', | ||
| 214 | enumList: commonEnumProviders.boolean()(), | ||
| 215 | }), | ||
| 216 | SlashCommandNamedArgument.fromProps({ | ||
| 217 | name: 'toast', | ||
| 218 | description: 'Toast display mode: stoppable (with stop button), static (no stop button), or none', | ||
| 219 | typeList: [ARGUMENT_TYPE.STRING], | ||
| 220 | defaultValue: ActionLoaderToastMode.STOPPABLE, | ||
| 221 | enumList: loaderEnumProviders.toastModeEnumProvider(), | ||
| 222 | }), | ||
| 223 | SlashCommandNamedArgument.fromProps({ | ||
| 224 | name: 'message', | ||
| 225 | description: 'Message to display in the toast notification', | ||
| 226 | typeList: [ARGUMENT_TYPE.STRING], | ||
| 227 | defaultValue: 'Generating...', | ||
| 228 | }), | ||
| 229 | SlashCommandNamedArgument.fromProps({ | ||
| 230 | name: 'title', | ||
| 231 | description: 'Optional title for the toast notification', | ||
| 232 | typeList: [ARGUMENT_TYPE.STRING], | ||
| 233 | }), | ||
| 234 | SlashCommandNamedArgument.fromProps({ | ||
| 235 | name: 'stopTooltip', | ||
| 236 | description: 'Tooltip text for the stop button (only used when toast=stoppable)', | ||
| 237 | typeList: [ARGUMENT_TYPE.STRING], | ||
| 238 | defaultValue: 'Stop', | ||
| 239 | }), | ||
| 240 | SlashCommandNamedArgument.fromProps({ | ||
| 241 | name: 'onStop', | ||
| 242 | description: 'Closure to execute when the stop button is clicked', | ||
| 243 | typeList: [ARGUMENT_TYPE.CLOSURE], | ||
| 244 | }), | ||
| 245 | SlashCommandNamedArgument.fromProps({ | ||
| 246 | name: 'onHide', | ||
| 247 | description: 'Closure to execute when the loader is hidden (not stopped)', | ||
| 248 | typeList: [ARGUMENT_TYPE.CLOSURE], | ||
| 249 | }), | ||
| 250 | ], | ||
| 251 | unnamedArgumentList: [], | ||
| 252 | callback: async (args) => { | ||
| 253 | const blocking = !isFalseBoolean(String(args.blocking)); | ||
| 254 | const toastMode = Object.values(ActionLoaderToastMode).includes(String(args.toast)) | ||
| 255 | ? String(args.toast) | ||
| 256 | : ActionLoaderToastMode.STOPPABLE; | ||
| 257 | const message = String(args.message ?? t`Generating...`); | ||
| 258 | const title = args.title ? String(args.title) : ''; | ||
| 259 | const stopTooltip = String(args.stopTooltip ?? t`Stop`); | ||
| 260 | |||
| 261 | const handle = showActionLoader({ | ||
| 262 | blocking, | ||
| 263 | toastMode, | ||
| 264 | message, | ||
| 265 | title, | ||
| 266 | stopTooltip, | ||
| 267 | onStop: createClosureHandler(args.onStop), | ||
| 268 | onHide: createClosureHandler(args.onHide, { argName: 'onHide' }), | ||
| 269 | }); | ||
| 270 | |||
| 271 | return handle.id; | ||
| 272 | }, | ||
| 273 | })); | ||
| 274 | |||
| 275 | // /loader-hide command - manually hide a loader by handle ID | ||
| 276 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | ||
| 277 | name: 'loader-hide', | ||
| 278 | returns: 'true if an active loader was hidden, otherwise false', | ||
| 279 | helpString: ` | ||
| 280 | <div> | ||
| 281 | Hides an action loader that was shown with <code>/loader-show</code>. | ||
| 282 | If no handle is provided, hides <strong>all</strong> active loaders. | ||
| 283 | </div> | ||
| 284 | <div> | ||
| 285 | <strong>Example:</strong> | ||
| 286 | <pre><code class="language-stscript">/loader-hide handle={{getvar::myLoader}}</code></pre> | ||
| 287 | </div> | ||
| 288 | `, | ||
| 289 | namedArgumentList: [ | ||
| 290 | SlashCommandNamedArgument.fromProps({ | ||
| 291 | name: 'handle', | ||
| 292 | description: 'Loader handle ID returned by /loader-show. If not provided, hides all active loaders.', | ||
| 293 | typeList: [ARGUMENT_TYPE.STRING], | ||
| 294 | enumProvider: loaderEnumProviders.loaderHandleProvider, | ||
| 295 | }), | ||
| 296 | ], | ||
| 297 | callback: async (args) => { | ||
| 298 | const handleId = args.handle ? String(args.handle) : null; | ||
| 299 | |||
| 300 | if (handleId) { | ||
| 301 | const handle = getLoaderHandleById(handleId); | ||
| 302 | if (handle && handle.isActive) { | ||
| 303 | await handle.hide(); | ||
| 304 | return 'true'; | ||
| 305 | } | ||
| 306 | return 'false'; | ||
| 307 | } | ||
| 308 | |||
| 309 | // No handle provided - hide all active loaders | ||
| 310 | const result = await hideActionLoader(); | ||
| 311 | return result ? 'true' : 'false'; | ||
| 312 | }, | ||
| 313 | })); | ||
| 314 | |||
| 315 | // /loader-stop command - trigger the stop action on a loader | ||
| 316 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | ||
| 317 | name: 'loader-stop', | ||
| 318 | returns: 'true if an active loader was stopped, otherwise false', | ||
| 319 | helpString: ` | ||
| 320 | <div> | ||
| 321 | Triggers the stop action on a specific action loader, as if the user clicked the stop button. | ||
| 322 | Unlike <code>/loader-hide</code>, this command requires a handle - you must specify which loader to stop. | ||
| 323 | </div> | ||
| 324 | <div> | ||
| 325 | <strong>Example:</strong> | ||
| 326 | <pre><code class="language-stscript">/loader-stop handle={{getvar::myLoader}}</code></pre> | ||
| 327 | </div> | ||
| 328 | `, | ||
| 329 | namedArgumentList: [ | ||
| 330 | SlashCommandNamedArgument.fromProps({ | ||
| 331 | name: 'handle', | ||
| 332 | description: 'Loader handle ID returned by /loader-show.', | ||
| 333 | typeList: [ARGUMENT_TYPE.STRING], | ||
| 334 | isRequired: true, | ||
| 335 | enumProvider: loaderEnumProviders.loaderHandleProvider, | ||
| 336 | }), | ||
| 337 | ], | ||
| 338 | callback: async (args) => { | ||
| 339 | const handleId = args.handle ? String(args.handle) : null; | ||
| 340 | |||
| 341 | if (!handleId) { | ||
| 342 | toastr.warning(t`No handle provided. You must specify which loader to stop.`); | ||
| 343 | return 'false'; | ||
| 344 | } | ||
| 345 | |||
| 346 | const handle = getLoaderHandleById(handleId); | ||
| 347 | if (handle && handle.isActive) { | ||
| 348 | await handle.stop(); | ||
| 349 | return 'true'; | ||
| 350 | } | ||
| 351 | |||
| 352 | return 'false'; | ||
| 353 | }, | ||
| 354 | })); | ||
| 355 | } | ||
| @@ -0,0 +1,406 @@ | |||
| 1 | /** | ||
| 2 | * Action loader utility - shows loader overlay with stoppable toast notification. | ||
| 3 | * Designed to be flexible and reusable for various long-running operations. | ||
| 4 | * Supports stacking multiple loaders - overlay stays single, but toasts can stack. | ||
| 5 | * | ||
| 6 | * With default arguments, will function as a generation loader / wrapper. | ||
| 7 | * | ||
| 8 | * @module action-loader | ||
| 9 | */ | ||
| 10 | |||
| 11 | import { t } from './i18n.js'; | ||
| 12 | import { stopGeneration } from '../script.js'; | ||
| 13 | import { showLoader, hideLoader, isLoaderDisplayed } from './loader.js'; | ||
| 14 | |||
| 15 | /** | ||
| 16 | * Enum representing the toast display mode for the action loader. | ||
| 17 | * @readonly | ||
| 18 | * @enum {string} | ||
| 19 | */ | ||
| 20 | export const ActionLoaderToastMode = { | ||
| 21 | /** No toast is displayed */ | ||
| 22 | NONE: 'none', | ||
| 23 | /** Toast is displayed without stop button (non-interactable) */ | ||
| 24 | STATIC: 'static', | ||
| 25 | /** Toast is displayed with stop button (default) */ | ||
| 26 | STOPPABLE: 'stoppable', | ||
| 27 | }; | ||
| 28 | |||
| 29 | /** | ||
| 30 | * @typedef {object} ActionLoaderOptions | ||
| 31 | * @property {boolean} [blocking=true] - Whether to show the blocking overlay. Set to false for non-blocking toast-only loaders. | ||
| 32 | * @property {ActionLoaderToastMode} [toastMode='stoppable'] - Toast display mode | ||
| 33 | * @property {string} [message='Generating...'] - The message to display in the toast | ||
| 34 | * @property {string} [title] - Optional title for the toast notification | ||
| 35 | * @property {string} [stopTooltip='Stop'] - Tooltip text for the stop button | ||
| 36 | * @property {(() => void)|null} [onStop=null] - Custom stop handler. If null, calls `stopGeneration()` | ||
| 37 | * @property {(() => void)|null} [onHide=null] - Custom hide handler. Called when the loader is hidden (not stopped). | ||
| 38 | */ | ||
| 39 | |||
| 40 | /** Counter for generating unique loader IDs */ | ||
| 41 | let loaderIdCounter = 0; | ||
| 42 | |||
| 43 | /** @type {Set<ActionLoaderHandle>} Set of all active loader handles */ | ||
| 44 | const activeHandles = new Set(); | ||
| 45 | |||
| 46 | /** | ||
| 47 | * Generates a unique loader ID. | ||
| 48 | * @returns {string} Unique loader ID | ||
| 49 | */ | ||
| 50 | function generateLoaderId() { | ||
| 51 | return `loader_${++loaderIdCounter}`; | ||
| 52 | } | ||
| 53 | |||
| 54 | /** | ||
| 55 | * Checks if there are any active blocking loaders. | ||
| 56 | * @returns {boolean} True if at least one blocking loader is active | ||
| 57 | */ | ||
| 58 | function hasBlockingLoaders() { | ||
| 59 | for (const handle of activeHandles) { | ||
| 60 | if (handle.isBlocking && handle.isActive) { | ||
| 61 | return true; | ||
| 62 | } | ||
| 63 | } | ||
| 64 | return false; | ||
| 65 | } | ||
| 66 | |||
| 67 | /** | ||
| 68 | * Class representing an action loader handle. | ||
| 69 | * Manages its own toast, stop handler, and lifecycle. | ||
| 70 | */ | ||
| 71 | export class ActionLoaderHandle { | ||
| 72 | /** @type {string} Unique identifier for this handle */ | ||
| 73 | id; | ||
| 74 | |||
| 75 | /** @type {JQuery<HTMLElement>|null} The toast element for this loader */ | ||
| 76 | #toast = null; | ||
| 77 | |||
| 78 | /** @type {(() => void)|null} Custom stop handler */ | ||
| 79 | #onStop = null; | ||
| 80 | |||
| 81 | /** @type {(() => void)|null} Custom hide handler */ | ||
| 82 | #onHide = null; | ||
| 83 | |||
| 84 | /** @type {boolean} Whether this loader blocks the UI with an overlay */ | ||
| 85 | #blocking = true; | ||
| 86 | |||
| 87 | /** @type {boolean} Whether this handle has been disposed */ | ||
| 88 | #disposed = false; | ||
| 89 | |||
| 90 | /** | ||
| 91 | * Creates a new ActionLoaderHandle. | ||
| 92 | * @param {object} options - Configuration options | ||
| 93 | * @param {boolean} [options.blocking=true] - Whether to show blocking overlay | ||
| 94 | * @param {ActionLoaderToastMode} [options.toastMode] - Toast display mode | ||
| 95 | * @param {string} [options.message='Generating...'] - Message to display in the toast | ||
| 96 | * @param {string} [options.title] - Title for the toast notification | ||
| 97 | * @param {string} [options.stopTooltip='Stop'] - Tooltip for the stop button | ||
| 98 | * @param {(() => void)|null} [options.onStop] - Custom stop handler | ||
| 99 | * @param {(() => void)|null} [options.onHide] - Custom hide handler | ||
| 100 | */ | ||
| 101 | constructor({ | ||
| 102 | blocking = true, | ||
| 103 | toastMode = ActionLoaderToastMode.STOPPABLE, | ||
| 104 | message = t`Generating...`, | ||
| 105 | title = '', | ||
| 106 | stopTooltip = t`Stop`, | ||
| 107 | onStop = null, | ||
| 108 | onHide = null, | ||
| 109 | } = {}) { | ||
| 110 | this.id = generateLoaderId(); | ||
| 111 | this.#blocking = blocking; | ||
| 112 | this.#onStop = onStop; | ||
| 113 | this.#onHide = onHide; | ||
| 114 | |||
| 115 | // Warn if non-blocking loader has no toast - it won't be visible to the user | ||
| 116 | if (!blocking && toastMode === ActionLoaderToastMode.NONE) { | ||
| 117 | console.warn('[ActionLoader] Non-blocking loader created without a toast. This loader will not be visible to the user.'); | ||
| 118 | } | ||
| 119 | |||
| 120 | // Show the blocking loader overlay if this is the first blocking handle | ||
| 121 | if (blocking && !hasBlockingLoaders() && !isLoaderDisplayed()) { | ||
| 122 | showLoader(); | ||
| 123 | } | ||
| 124 | |||
| 125 | // Register this handle | ||
| 126 | activeHandles.add(this); | ||
| 127 | |||
| 128 | // Create toast if needed | ||
| 129 | if (toastMode !== ActionLoaderToastMode.NONE) { | ||
| 130 | this.#createToast(message, title, toastMode, stopTooltip); | ||
| 131 | } | ||
| 132 | } | ||
| 133 | |||
| 134 | /** | ||
| 135 | * Creates the toast element for this loader. | ||
| 136 | * @param {string} message - Message to display | ||
| 137 | * @param {string} title - Title for the toast | ||
| 138 | * @param {ActionLoaderToastMode} toastMode - Toast mode | ||
| 139 | * @param {string} stopTooltip - Tooltip for stop button | ||
| 140 | */ | ||
| 141 | #createToast(message, title, toastMode, stopTooltip) { | ||
| 142 | const toastContent = document.createElement('div'); | ||
| 143 | toastContent.className = 'action-loader-toast'; | ||
| 144 | |||
| 145 | const messageSpan = document.createElement('span'); | ||
| 146 | messageSpan.className = 'action-loader-message'; | ||
| 147 | messageSpan.textContent = message; | ||
| 148 | toastContent.appendChild(messageSpan); | ||
| 149 | |||
| 150 | // Add stop button if mode is STOPPABLE | ||
| 151 | if (toastMode === ActionLoaderToastMode.STOPPABLE) { | ||
| 152 | const stopButton = document.createElement('i'); | ||
| 153 | stopButton.className = 'fa-solid fa-stop-circle action-loader-stop interactable'; | ||
| 154 | stopButton.title = stopTooltip; | ||
| 155 | stopButton.addEventListener('click', (e) => { | ||
| 156 | e.preventDefault(); | ||
| 157 | e.stopPropagation(); | ||
| 158 | this.stop(); | ||
| 159 | }); | ||
| 160 | toastContent.appendChild(stopButton); | ||
| 161 | } | ||
| 162 | |||
| 163 | // Show toast with no timeout (sticky) | ||
| 164 | this.#toast = toastr.info($(toastContent), title, { | ||
| 165 | timeOut: 0, | ||
| 166 | extendedTimeOut: 0, | ||
| 167 | tapToDismiss: false, | ||
| 168 | escapeHtml: false, | ||
| 169 | }); | ||
| 170 | } | ||
| 171 | |||
| 172 | /** | ||
| 173 | * Clears the toast element for this loader. | ||
| 174 | */ | ||
| 175 | #clearToast() { | ||
| 176 | if (this.#toast) { | ||
| 177 | toastr.clear(this.#toast, { force: true }); // Need to force as the toast might have focus/hover | ||
| 178 | this.#toast = null; | ||
| 179 | } | ||
| 180 | } | ||
| 181 | |||
| 182 | /** | ||
| 183 | * Disposes this handle, removing it from active handles and hiding overlay if last. | ||
| 184 | */ | ||
| 185 | async #dispose() { | ||
| 186 | if (this.#disposed) return; | ||
| 187 | this.#disposed = true; | ||
| 188 | |||
| 189 | this.#clearToast(); | ||
| 190 | activeHandles.delete(this); | ||
| 191 | |||
| 192 | // Hide the overlay if this was the last blocking handle | ||
| 193 | if (this.#blocking && !hasBlockingLoaders()) { | ||
| 194 | await hideLoader(); | ||
| 195 | } | ||
| 196 | } | ||
| 197 | |||
| 198 | /** | ||
| 199 | * Whether this handle is still active (not disposed). | ||
| 200 | * @returns {boolean} | ||
| 201 | */ | ||
| 202 | get isActive() { | ||
| 203 | return !this.#disposed; | ||
| 204 | } | ||
| 205 | |||
| 206 | /** | ||
| 207 | * Whether this loader blocks the UI with an overlay. | ||
| 208 | * @returns {boolean} | ||
| 209 | */ | ||
| 210 | get isBlocking() { | ||
| 211 | return this.#blocking; | ||
| 212 | } | ||
| 213 | |||
| 214 | /** | ||
| 215 | * Triggers the stop action on this loader. | ||
| 216 | * Calls the custom onStop handler if provided, otherwise calls stopGeneration(). | ||
| 217 | * Then hides this loader. | ||
| 218 | */ | ||
| 219 | async stop() { | ||
| 220 | if (this.#disposed) return; | ||
| 221 | |||
| 222 | // Call custom stop handler or default | ||
| 223 | if (this.#onStop) { | ||
| 224 | try { | ||
| 225 | await this.#onStop(); | ||
| 226 | } catch (e) { | ||
| 227 | console.error('Error executing onStop handler', e); | ||
| 228 | } | ||
| 229 | } else { | ||
| 230 | stopGeneration(); | ||
| 231 | } | ||
| 232 | |||
| 233 | // Dispose without calling onHide (stop is different from hide) | ||
| 234 | await this.#dispose(); | ||
| 235 | } | ||
| 236 | |||
| 237 | /** | ||
| 238 | * Hides this loader and clears its toast. | ||
| 239 | * Calls the custom onHide handler if provided. | ||
| 240 | */ | ||
| 241 | async hide() { | ||
| 242 | if (this.#disposed) return; | ||
| 243 | |||
| 244 | // Call custom hide handler if provided | ||
| 245 | if (this.#onHide) { | ||
| 246 | try { | ||
| 247 | await this.#onHide(); | ||
| 248 | } catch (e) { | ||
| 249 | console.error('Error executing onHide handler', e); | ||
| 250 | } | ||
| 251 | } | ||
| 252 | |||
| 253 | await this.#dispose(); | ||
| 254 | } | ||
| 255 | } | ||
| 256 | |||
| 257 | /** | ||
| 258 | * Action loader utility API. | ||
| 259 | * Provides a convenient interface for showing and managing loading indicators. | ||
| 260 | * | ||
| 261 | * Read the functions documentation for more details. | ||
| 262 | * | ||
| 263 | * @example | ||
| 264 | * // Basic usage | ||
| 265 | * const handle = loader.show({ message: 'Loading...' }); | ||
| 266 | * await someOperation(); | ||
| 267 | * handle.hide(); | ||
| 268 | * | ||
| 269 | * @example | ||
| 270 | * // Non-blocking background task | ||
| 271 | * const handle = loader.show({ blocking: false, message: 'Processing...' }); | ||
| 272 | * | ||
| 273 | * @example | ||
| 274 | * // Hide all active loaders | ||
| 275 | * loader.hide(); | ||
| 276 | */ | ||
| 277 | export const loader = { | ||
| 278 | /** | ||
| 279 | * Shows an action loader with optional toast notification. | ||
| 280 | * Returns a handle to control the loader. | ||
| 281 | * @type {typeof showActionLoader} | ||
| 282 | */ | ||
| 283 | show: showActionLoader, | ||
| 284 | |||
| 285 | /** | ||
| 286 | * Hides a specific loader by handle, or all loaders if no handle provided. | ||
| 287 | * @type {typeof hideActionLoader} | ||
| 288 | */ | ||
| 289 | hide: hideActionLoader, | ||
| 290 | |||
| 291 | /** | ||
| 292 | * Gets all currently active loader handles. | ||
| 293 | * @type {typeof getActiveLoaderHandles} | ||
| 294 | */ | ||
| 295 | active: getActiveLoaderHandles, | ||
| 296 | |||
| 297 | /** | ||
| 298 | * Gets a loader handle by its ID. | ||
| 299 | * @type {typeof getLoaderHandleById} | ||
| 300 | */ | ||
| 301 | get: getLoaderHandleById, | ||
| 302 | |||
| 303 | /** | ||
| 304 | * Toast display mode constants. | ||
| 305 | * @type {typeof ActionLoaderToastMode} | ||
| 306 | */ | ||
| 307 | ToastMode: ActionLoaderToastMode, | ||
| 308 | |||
| 309 | /** | ||
| 310 | * The ActionLoaderHandle class. | ||
| 311 | * @type {typeof ActionLoaderHandle} | ||
| 312 | */ | ||
| 313 | Handle: ActionLoaderHandle, | ||
| 314 | }; | ||
| 315 | |||
| 316 | /** | ||
| 317 | * Shows an action loader with an optional stoppable toast notification. | ||
| 318 | * Multiple loaders can be stacked - the overlay stays single, but each gets its own toast. | ||
| 319 | * When the last loader is hidden, the overlay is removed. | ||
| 320 | * | ||
| 321 | * With default arguments, will function as a generation loader / wrapper. | ||
| 322 | * | ||
| 323 | * @param {ActionLoaderOptions} [options={}] - Configuration options | ||
| 324 | * @returns {ActionLoaderHandle} Handle to control the loader | ||
| 325 | * | ||
| 326 | * @example | ||
| 327 | * // Basic usage | ||
| 328 | * const loader = showActionLoader({ message: 'Generating title...' }); | ||
| 329 | * try { | ||
| 330 | * const result = await generateRaw({ prompt }); | ||
| 331 | * // process result | ||
| 332 | * } finally { | ||
| 333 | * await loader.hide(); | ||
| 334 | * } | ||
| 335 | * | ||
| 336 | * @example | ||
| 337 | * // With custom stop and hide handlers | ||
| 338 | * const loader = showActionLoader({ | ||
| 339 | * message: 'Downloading...', | ||
| 340 | * stopTooltip: 'Cancel download', | ||
| 341 | * onStop: () => myCustomCancelFunction(), | ||
| 342 | * onHide: () => console.log('Loader hidden'), | ||
| 343 | * }); | ||
| 344 | * | ||
| 345 | * @example | ||
| 346 | * // Stacking multiple loaders | ||
| 347 | * const loader1 = showActionLoader({ message: 'Task 1...' }); | ||
| 348 | * const loader2 = showActionLoader({ message: 'Task 2...' }); | ||
| 349 | * await loader1.hide(); // Overlay stays, loader2 still active | ||
| 350 | * await loader2.hide(); // Now overlay hides | ||
| 351 | * | ||
| 352 | * @example | ||
| 353 | * // Non-blocking loader (toast only, no overlay) | ||
| 354 | * const loader = showActionLoader({ | ||
| 355 | * message: 'Captioning image...', | ||
| 356 | * blocking: false, | ||
| 357 | * onStop: () => abortCaptioning(), | ||
| 358 | * }); | ||
| 359 | */ | ||
| 360 | export function showActionLoader(options = {}) { | ||
| 361 | return new ActionLoaderHandle(options); | ||
| 362 | } | ||
| 363 | |||
| 364 | /** | ||
| 365 | * Hides a specific action loader by handle, or all active loaders if no handle provided. | ||
| 366 | * @param {ActionLoaderHandle|null} [handle=null] - Specific handle to hide, or undefined to hide all | ||
| 367 | * @returns {Promise<boolean>} Whether any loader was hidden | ||
| 368 | */ | ||
| 369 | export async function hideActionLoader(handle = null) { | ||
| 370 | if (handle instanceof ActionLoaderHandle) { | ||
| 371 | if (handle.isActive) { | ||
| 372 | await handle.hide(); | ||
| 373 | return true; | ||
| 374 | } | ||
| 375 | return false; | ||
| 376 | } | ||
| 377 | |||
| 378 | // No handle provided - hide all active loaders | ||
| 379 | const handles = getActiveLoaderHandles(); | ||
| 380 | for (const h of handles) { | ||
| 381 | await h.hide(); | ||
| 382 | } | ||
| 383 | return handles.length > 0; | ||
| 384 | } | ||
| 385 | |||
| 386 | /** | ||
| 387 | * Gets all currently active loader handles. | ||
| 388 | * @returns {ActionLoaderHandle[]} Array of active handles | ||
| 389 | */ | ||
| 390 | export function getActiveLoaderHandles() { | ||
| 391 | return Array.from(activeHandles); | ||
| 392 | } | ||
| 393 | |||
| 394 | /** | ||
| 395 | * Gets a loader handle by its ID. | ||
| 396 | * @param {string} id - The handle ID | ||
| 397 | * @returns {ActionLoaderHandle|undefined} The handle, or undefined if not found | ||
| 398 | */ | ||
| 399 | export function getLoaderHandleById(id) { | ||
| 400 | for (const handle of activeHandles) { | ||
| 401 | if (handle.id === id) { | ||
| 402 | return handle; | ||
| 403 | } | ||
| 404 | } | ||
| 405 | return undefined; | ||
| 406 | } | ||
| @@ -5,6 +5,10 @@ let loaderPopup; | |||
| 5 | 5 | ||
| 6 | let preloaderYoinked = false; | 6 | let preloaderYoinked = false; |
| 7 | 7 | ||
| 8 | export function isLoaderDisplayed() { | ||
| 9 | return !!loaderPopup; | ||
| 10 | } | ||
| 11 | |||
| 8 | export function showLoader() { | 12 | export function showLoader() { |
| 9 | // Two loaders don't make sense. Don't await, we can overlay the old loader while it closes | 13 | // Two loaders don't make sense. Don't await, we can overlay the old loader while it closes |
| 10 | if (loaderPopup) loaderPopup.complete(POPUP_RESULT.CANCELLED); | 14 | if (loaderPopup) loaderPopup.complete(POPUP_RESULT.CANCELLED); |
| @@ -77,6 +77,7 @@ import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './te | |||
| 77 | import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js'; | 77 | import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js'; |
| 78 | import { debounce, delay, equalsIgnoreCaseAndAccents, findChar, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, regexFromString, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js'; | 78 | import { debounce, delay, equalsIgnoreCaseAndAccents, findChar, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, regexFromString, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js'; |
| 79 | import { registerVariableCommands, resolveVariable } from './variables.js'; | 79 | import { registerVariableCommands, resolveVariable } from './variables.js'; |
| 80 | import { registerActionLoaderSlashCommands } from './action-loader-slashcommands.js'; | ||
| 80 | import { background_settings } from './backgrounds.js'; | 81 | import { background_settings } from './backgrounds.js'; |
| 81 | import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js'; | 82 | import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js'; |
| 82 | import { SlashCommandClosureResult } from './slash-commands/SlashCommandClosureResult.js'; | 83 | import { SlashCommandClosureResult } from './slash-commands/SlashCommandClosureResult.js'; |
| @@ -3714,6 +3715,7 @@ export function initDefaultSlashCommands() { | |||
| 3714 | })); | 3715 | })); |
| 3715 | 3716 | ||
| 3716 | registerVariableCommands(); | 3717 | registerVariableCommands(); |
| 3718 | registerActionLoaderSlashCommands(); | ||
| 3717 | } | 3719 | } |
| 3718 | 3720 | ||
| 3719 | const NARRATOR_NAME_KEY = 'narrator_name'; | 3721 | const NARRATOR_NAME_KEY = 'narrator_name'; |
| @@ -42,6 +42,8 @@ export const enumIcons = { | |||
| 42 | image: '๐ผ๏ธ', | 42 | image: '๐ผ๏ธ', |
| 43 | video: '๐ฅ', | 43 | video: '๐ฅ', |
| 44 | key: '๐', | 44 | key: '๐', |
| 45 | spinner: 'โป๏ธ', | ||
| 46 | stop: '๐', | ||
| 45 | 47 | ||
| 46 | true: 'โ๏ธ', | 48 | true: 'โ๏ธ', |
| 47 | false: 'โ', | 49 | false: 'โ', |
| @@ -81,6 +81,7 @@ import { | |||
| 81 | import { groups, openGroupChat, selected_group, unshallowGroupMembers } from './group-chats.js'; | 81 | import { groups, openGroupChat, selected_group, unshallowGroupMembers } from './group-chats.js'; |
| 82 | import { addLocaleData, getCurrentLocale, t, translate } from './i18n.js'; | 82 | import { addLocaleData, getCurrentLocale, t, translate } from './i18n.js'; |
| 83 | import { hideLoader, showLoader } from './loader.js'; | 83 | import { hideLoader, showLoader } from './loader.js'; |
| 84 | import { loader } from './action-loader.js'; | ||
| 84 | import { MacrosParser } from './macros.js'; | 85 | import { MacrosParser } from './macros.js'; |
| 85 | import { getChatCompletionModel, oai_settings } from './openai.js'; | 86 | import { getChatCompletionModel, oai_settings } from './openai.js'; |
| 86 | import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js'; | 87 | import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js'; |
| @@ -235,6 +236,7 @@ export function getContext() { | |||
| 235 | scrollChatToBottom, | 236 | scrollChatToBottom, |
| 236 | scrollOnMediaLoad, | 237 | scrollOnMediaLoad, |
| 237 | macros, | 238 | macros, |
| 239 | loader, | ||
| 238 | swipe: { | 240 | swipe: { |
| 239 | left: swipe_left, | 241 | left: swipe_left, |
| 240 | right: swipe_right, | 242 | right: swipe_right, |
| @@ -3973,7 +3973,7 @@ grammarly-extension { | |||
| 3973 | 3973 | ||
| 3974 | 3974 | ||
| 3975 | /* Override toastr default styles */ | 3975 | /* Override toastr default styles */ |
| 3976 | body>#toast-container { | 3976 | body #toast-container { |
| 3977 | margin-top: var(--topBarBlockSize); | 3977 | margin-top: var(--topBarBlockSize); |
| 3978 | } | 3978 | } |
| 3979 | 3979 | ||
| @@ -3986,6 +3986,10 @@ body #toast-container>div { | |||
| 3986 | width: 300px; | 3986 | width: 300px; |
| 3987 | } | 3987 | } |
| 3988 | 3988 | ||
| 3989 | body #toast-container .toast.toast-non-interactable { | ||
| 3990 | cursor: default; | ||
| 3991 | } | ||
| 3992 | |||
| 3989 | body #toast-container .toast-success { | 3993 | body #toast-container .toast-success { |
| 3990 | background-color: #5d9e5d; | 3994 | background-color: #5d9e5d; |
| 3991 | } | 3995 | } |