diff --git a/css/art/genAI.css b/css/art/genAI.css index c13b959d1b2e617510b2d836490e56a132c55734..a0d86a67104420a4767406fcf205bf7e89e01d36 100644 --- a/css/art/genAI.css +++ b/css/art/genAI.css @@ -49,7 +49,7 @@ transition: opacity 300ms ease-in-out; } -.spinner { +.ai-spinner { display: none; position: absolute; top: 50%; @@ -83,7 +83,7 @@ object-fit: contain; } -.ai-art-container.refreshing .spinner { +.ai-art-container.refreshing .ai-spinner { display: block; } @@ -164,3 +164,44 @@ transform: translate(-50%, -50%) rotate(360deg); } } + +.ai-queue-overlay { + position: fixed; + right: 0; + bottom: 0; + background-color: #1a1a1a; + border-left: #333 2px solid; + border-top: #333 2px solid; + border-top-left-radius: 1em; + padding: 0.5em; + +} + +.ai-queue-overlay.hidden { + display: none; +} + +.ai-queue-overlay .spinner { + display: inline-block; + width: 2.3em; +} + +.ai-queue-overlay .spinner::after { + font-family: 'sc-icons'; + content: "\f110"; + position: absolute; + top: 50%; + left: 0.9em; + font-size: 25px; + animation: spin 3s linear infinite; +} + +.ai-queue-overlay button { + font-family: 'sc-icons'; + background-color: var(--button-color); + border: solid 2px var(--button-border-color); +} + +.ai-queue-overlay button:hover { + background-color: var(--button-hover-color); +} diff --git a/js/003-data/gameVariableData.js b/js/003-data/gameVariableData.js index 6b703ed3408f80050100e33bcaf35547f4a40a05..fe7bb6eda1451d71806b1f5ada029819b741f4c2 100644 --- a/js/003-data/gameVariableData.js +++ b/js/003-data/gameVariableData.js @@ -285,6 +285,7 @@ App.Data.defaultGameStateVariables = { aiApiUrl: "http://localhost:7860", aiAutoGen: true, aiAutoGenFrequency: 10, + aiQueueOverlay: 1, aiUseRAForEvents: false, aiCfgScale: 5, aiTimeoutPerStep: 5, diff --git a/src/art/artJS.js b/src/art/artJS.js index 63c20dfde2f39a3ba44467322bf3b02b0d003ffa..d013dbc40c2e8d75f159aef9ce5ceb4bd4abd498 100644 --- a/src/art/artJS.js +++ b/src/art/artJS.js @@ -599,7 +599,7 @@ App.Art.aiArtElement = function(slave, imageSize, isEventImage = null) { // Loading spinner // eslint-disable-next-line no-unused-vars - spinner = App.UI.DOM.appendNewElement('div', container, '⟳', ['spinner']); + spinner = App.UI.DOM.appendNewElement('div', container, '⟳', ['ai-spinner']); const reactiveSpecific = { diff --git a/src/art/genAI/queueOverlay.js b/src/art/genAI/queueOverlay.js new file mode 100644 index 0000000000000000000000000000000000000000..2c04267a164441b43f73c1003cb51c084c05ecb3 --- /dev/null +++ b/src/art/genAI/queueOverlay.js @@ -0,0 +1,61 @@ +App.Art.GenAI.UI.QueueOverlay = function() { + // Internal State + let toggleVisible = true; + let generating = false; + + // Setup containers + const container = document.createElement("div"); + container.classList.add("ai-queue-overlay"); + document.body.append(container); + + const mainQueueSpan = document.createElement("span"); + const backlogSpan = document.createElement("span"); + + + // Export functionality + return {init: init, toggle: toggle}; + + function init() { + // Setup content + const spinnerSpan = document.createElement("span"); + spinnerSpan.classList.add("spinner"); + container.append(spinnerSpan); + + container.append("(Queue: ", mainQueueSpan, " / Backlog: ", backlogSpan, ") "); + + const button = document.createElement("button"); + button.append("\uf410"); + button.onclick = () => App.Art.GenAI.sdQueue.interrupt(); + container.append(button); + + // Callback + App.Art.GenAI.sdQueue.registerStatusChangeCallback(queueChangeCallback); + } + + /** + * @param {ArtQueueState} status + */ + function queueChangeCallback(status) { + mainQueueSpan.textContent = String(status.mainQueueCount); + backlogSpan.textContent = String(status.backlogCount); + generating = status.active; + updateVisible(); + } + + /** + * @param {boolean} visible + */ + function toggle(visible) { + toggleVisible = visible; + updateVisible(); + } + + function updateVisible() { + const visible = V.aiQueueOverlay === 1 && toggleVisible && generating; + if (visible) { + container.classList.remove("hidden"); + } else { + container.classList.add("hidden"); + } + } +}(); diff --git a/src/art/genAI/stableDiffusion.js b/src/art/genAI/stableDiffusion.js index a9bc7e0a03cd608ca181c601a2ba103e0f5ede9b..8bdddef1f3e3b6756e6a871f48559400b9e366ab 100644 --- a/src/art/genAI/stableDiffusion.js +++ b/src/art/genAI/stableDiffusion.js @@ -111,6 +111,13 @@ async function fetchWithTimeout(url, timeout, options) { * @property {[function(string): void]} rejects */ +/** + * @typedef {object} ArtQueueState + * @property {boolean} active + * @property {number} mainQueueCount + * @property {number} backlogCount + */ + App.Art.GenAI.StableDiffusionClientQueue = class { constructor() { // Images for this current screen @@ -124,6 +131,11 @@ App.Art.GenAI.StableDiffusionClientQueue = class { this.workingOnID = null; /** @type {string|null} */ this.workingOnBody = null; + /** + * @type {Array<function(ArtQueueState): void>} + * @private + */ + this._statusChangeCallbacks = []; } resetWorkingOnProperties() { @@ -131,6 +143,15 @@ App.Art.GenAI.StableDiffusionClientQueue = class { this.workingOnID = null; } + /** + * First value is main queue, second backlog queue + * + * @param {function(ArtQueueState): void} callback + */ + registerStatusChangeCallback(callback) { + this._statusChangeCallbacks.push(callback); + } + /** * Updates the queue counts if on the ai image settings page */ @@ -148,6 +169,14 @@ App.Art.GenAI.StableDiffusionClientQueue = class { queue.empty().append(count.toString()); } }); + const state = { + active: this.workingOnID !== null, + mainQueueCount: this.queue.length, + backlogCount: this.backlogQueue.length + }; + for (const callback of this._statusChangeCallbacks) { + callback(state); + } } openWebSocket(top, options) { diff --git a/src/art/genAI/uiOptions.js b/src/art/genAI/uiOptions.js index 243096e7e6750aaea19a1066bfa151991c911ae5..2c21230c04ae1bbf57e235fd9d283fe6b92ec3ce 100644 --- a/src/art/genAI/uiOptions.js +++ b/src/art/genAI/uiOptions.js @@ -976,6 +976,9 @@ App.Art.GenAI.UI.Options.aiGenerationSettings = () => { } } + options.addOption("Generation Queue Overlay", "aiQueueOverlay") + .addValue("Enabled", 1).on().addValue("Disabled", 0).off(); + el.append(options.render()); return el; }; diff --git a/src/zz1-last/init.js b/src/zz1-last/init.js index 0bc001dd5bc5ceccf9d46740e236ac54d3a1cae7..2a3926cae3af44b8615213846ea8558b2c2feafd 100644 --- a/src/zz1-last/init.js +++ b/src/zz1-last/init.js @@ -1,3 +1,4 @@ App.Art.cacheArtData(); App.Corporate.Init(); App.RA.Activation.populateGetters(); +App.Art.GenAI.UI.QueueOverlay.init(); diff --git a/src/zz1-last/setupEventHandlers.js b/src/zz1-last/setupEventHandlers.js index cdf7277ca7d03566503f8d020d6163178510fc0d..e34f07aaf7a86e30f37636a0f40bcc9b7cb67307 100644 --- a/src/zz1-last/setupEventHandlers.js +++ b/src/zz1-last/setupEventHandlers.js @@ -22,6 +22,7 @@ $(document).on(":passagestart", event => { enumerable: true }); App?.Utils?.passageHistoryCleanup(); + App.Art.GenAI.UI.QueueOverlay.toggle(passage() !== "Options"); profileEvents.passagestart(); });