From 3fc93e2c57f023838edc8e521f0a671ba23b02ea Mon Sep 17 00:00:00 2001 From: Olivia Godone-Maresca Date: Mon, 3 Apr 2023 17:39:34 -0400 Subject: [PATCH 1/2] Add a utility function to create tabs with lazy loading functionality --- ui/media/js/utils.js | 136 +++++++- ui/plugins/ui/merge.plugin.js | 464 ++++++++++++-------------- ui/plugins/ui/release-notes.plugin.js | 71 ++-- 3 files changed, 384 insertions(+), 287 deletions(-) diff --git a/ui/media/js/utils.js b/ui/media/js/utils.js index 6eb0d643..f38cb5ac 100644 --- a/ui/media/js/utils.js +++ b/ui/media/js/utils.js @@ -683,7 +683,7 @@ class ServiceContainer { * @param {string} tag * @param {object} attributes * @param {string | Array} classes - * @param {string | HTMLElement | Array} + * @param {string | Node | Array} * @returns {HTMLElement} */ function createElement(tagName, attributes, classes, textOrElements) { @@ -699,7 +699,7 @@ function createElement(tagName, attributes, classes, textOrElements) { if (textOrElements) { const children = Array.isArray(textOrElements) ? textOrElements : [textOrElements] children.forEach(textOrElem => { - if (textOrElem instanceof HTMLElement) { + if (textOrElem instanceof Node) { element.appendChild(textOrElem) } else { element.appendChild(document.createTextNode(textOrElem)) @@ -708,3 +708,135 @@ function createElement(tagName, attributes, classes, textOrElements) { } return element } + +/** + * @typedef {object} TabOpenDetails + * @property {HTMLElement} contentElement + * @property {HTMLElement} labelElement + * @property {number} timesOpened + * @property {boolean} firstOpen + */ + +/** + * @typedef {object} CreateTabRequest + * @property {string} id + * @property {string | Node | (() => (string | Node))} label + * Label text or an HTML element + * @property {string} icon + * @property {string | Node | Promise | (() => (string | Node | Promise)) | undefined} content + * HTML string or HTML element + * @property {((TabOpenDetails, Event) => (undefined | string | Node | Promise)) | undefined} onOpen + * If an HTML string or HTML element is returned, then that will replace the tab content + * @property {string | undefined} css + */ + +/** + * @param {CreateTabRequest} request + */ + function createTab(request) { + if (!request?.id) { + console.error('createTab() error - id is required', Error().stack) + return + } + + if (!request.label) { + console.error('createTab() error - label is required', Error().stack) + return + } + + if (!request.icon) { + console.error('createTab() error - icon is required', Error().stack) + return + } + + if (!request.content && !request.onOpen) { + console.error('createTab() error - content or onOpen required', Error().stack) + return + } + + const tabsContainer = document.querySelector('.tab-container') + if (!tabsContainer) { + return + } + + const tabsContentWrapper = document.querySelector('#tab-content-wrapper') + if (!tabsContentWrapper) { + return + } + + console.debug('creating tab: ', request) + + if (request.css) { + document.querySelector('body').insertAdjacentElement( + 'beforeend', + createElement('style', { id: `tab-${request.id}-css` }, undefined, request.css), + ) + } + + const label = typeof request.label === 'function' ? request.label() : request.label + const labelElement = label instanceof Node ? label : createElement('span', undefined, undefined, label) + + const tab = createElement( + 'span', + { id: `tab-${request.id}`, 'data-times-opened': 0 }, + ['tab'], + createElement( + 'span', + undefined, + undefined, + [ + createElement( + 'i', + { style: 'margin-right: 0.25em' }, + ['fa-solid', `${request.icon.startsWith('fa-') ? '' : 'fa-'}${request.icon}`, 'icon'], + ), + labelElement, + ], + ) + ) + + + tabsContainer.insertAdjacentElement('beforeend', tab) + + const wrapper = createElement('div', { id: request.id }, ['tab-content-inner'], 'Loading..') + + const tabContent = createElement('div', { id: `tab-content-${request.id}` }, ['tab-content'], wrapper) + tabsContentWrapper.insertAdjacentElement('beforeend', tabContent) + + linkTabContents(tab) + + function replaceContent(resultFactory) { + if (resultFactory === undefined || resultFactory === null) { + return + } + const result = typeof resultFactory === 'function' ? resultFactory() : resultFactory + if (result instanceof Promise) { + result.then(replaceContent) + } else if (result instanceof Node) { + wrapper.replaceChildren(result) + } else { + wrapper.innerHTML = result + } + } + + replaceContent(request.content) + + tab.addEventListener('click', (e) => { + const timesOpened = +(tab.dataset.timesOpened || 0) + 1 + tab.dataset.timesOpened = timesOpened + + if (request.onOpen) { + const result = request.onOpen( + { + contentElement: wrapper, + labelElement, + timesOpened, + firstOpen: timesOpened === 1, + }, + e, + ) + + replaceContent(result) + } + }) +} diff --git a/ui/plugins/ui/merge.plugin.js b/ui/plugins/ui/merge.plugin.js index 6ff97286..4fce1d84 100644 --- a/ui/plugins/ui/merge.plugin.js +++ b/ui/plugins/ui/merge.plugin.js @@ -130,34 +130,11 @@ } drawDiagram(fn) } - - /////////////////////// Tab implementation - document.querySelector('.tab-container')?.insertAdjacentHTML('beforeend', ` - - Merge models - - `) - - document.querySelector('#tab-content-wrapper')?.insertAdjacentHTML('beforeend', ` -
-
- Loading.. -
-
- `) - - const tabMerge = document.querySelector('#tab-merge') - if (tabMerge) { - linkTabContents(tabMerge) - } - const merge = document.querySelector('#merge') - if (!merge) { - // merge tab not found, dont exec plugin code. - return - } - - document.querySelector('body').insertAdjacentHTML('beforeend', ` - - `) - - merge.innerHTML = ` -
-
-

- -

- -

-

Important: Please merge models of similar type.
For e.g. SD 1.4 models with only SD 1.4/1.5 models,
SD 2.0 with SD 2.0-type, and SD 2.1 with SD 2.1-type models.

-
- - - - - - - - - - - - - -
Base name of the output file.
Mix ratio and file suffix will be appended to this.
- Image generation uses fp16, so it's a good choice.
Use fp32 if you want to use the result models for more mixes
-
-
-
-
-

-
-
-
-
-
- - Make a single file - - - Make multiple variations - -
-
-
-
- Saves a single merged model file, at the specified merge ratio.

- - - % - Model A's contribution to the mix. The rest will be from Model B. + }`, + content: ` +
+
+

+ +

+ +

+

Important: Please merge models of similar type.
For e.g. SD 1.4 models with only SD 1.4/1.5 models,
SD 2.0 with SD 2.0-type, and SD 2.1 with SD 2.1-type models.

+
+ + + + + + + + + + + + + +
Base name of the output file.
Mix ratio and file suffix will be appended to this.
+ Image generation uses fp16, so it's a good choice.
Use fp32 if you want to use the result models for more mixes
+
+
+
+
+

+
+
+
+
+
+ + Make a single file + + + Make multiple variations + +
+
+
+
+ Saves a single merged model file, at the specified merge ratio.

+ + + % + Model A's contribution to the mix. The rest will be from Model B. +
-
-
-
- Saves multiple variations of the model, at different merge ratios.
Each variation will be saved as a separate file.


- - - - - - - - - - - - - -
Number of models to create
% Smallest share of model A in the mix
% Share of model A added into the mix per step
Sigmoid function to be applied to the model share before mixing
-
- Preview of variation ratios:
- +
+
+ Saves multiple variations of the model, at different merge ratios.
Each variation will be saved as a separate file.


+ + + + + + + + + + + + + +
Number of models to create
% Smallest share of model A in the mix
% Share of model A added into the mix per step
Sigmoid function to be applied to the model share before mixing
+
+ Preview of variation ratios:
+ +
-
-
-
-
- -
-
` - - const tabSettingsSingle = document.querySelector('#tab-merge-opts-single') - const tabSettingsBatch = document.querySelector('#tab-merge-opts-batch') - linkTabContents(tabSettingsSingle) - linkTabContents(tabSettingsBatch) - - console.log('Activate') - let mergeModelAField = new ModelDropdown(document.querySelector('#mergeModelA'), 'stable-diffusion') - let mergeModelBField = new ModelDropdown(document.querySelector('#mergeModelB'), 'stable-diffusion') - updateChart() - - // slider - const singleMergeRatioField = document.querySelector('#single-merge-ratio') - const singleMergeRatioSlider = document.querySelector('#single-merge-ratio-slider') - - function updateSingleMergeRatio() { - singleMergeRatioField.value = singleMergeRatioSlider.value / 10 - singleMergeRatioField.dispatchEvent(new Event("change")) - } - - function updateSingleMergeRatioSlider() { - if (singleMergeRatioField.value < 0) { - singleMergeRatioField.value = 0 - } else if (singleMergeRatioField.value > 100) { - singleMergeRatioField.value = 100 - } - - singleMergeRatioSlider.value = singleMergeRatioField.value * 10 - singleMergeRatioSlider.dispatchEvent(new Event("change")) - } - - singleMergeRatioSlider.addEventListener('input', updateSingleMergeRatio) - singleMergeRatioField.addEventListener('input', updateSingleMergeRatioSlider) - updateSingleMergeRatio() - - document.querySelector('.merge-config').addEventListener('change', updateChart) - - document.querySelector('#merge-button').addEventListener('click', async function(e) { - // Build request template - let model0 = mergeModelAField.value - let model1 = mergeModelBField.value - let request = { model0: model0, model1: model1 } - request['use_fp16'] = document.querySelector('#merge-fp').value == 'fp16' - let iterations = document.querySelector('#merge-count').value>>0 - let start = parseFloat( document.querySelector('#merge-start').value ) - let step = parseFloat( document.querySelector('#merge-step').value ) - - if (isTabActive(tabSettingsSingle)) { - start = parseFloat(singleMergeRatioField.value) - step = 0 - iterations = 1 - addLogMessage(`merge ratio = ${start}%`) - } else { - addLogMessage(`start = ${start}%`) - addLogMessage(`step = ${step}%`) - } - - if (start + (iterations-1) * step >= 100) { - addLogMessage('Aborting: maximum ratio is ≥ 100%') - addLogMessage('Reduce the number of variations or the step size') - addLogSeparator() - document.querySelector('#merge-count').focus() - return - } - - if (document.querySelector('#merge-filename').value == "") { - addLogMessage('Aborting: No output file name specified') - addLogSeparator() - document.querySelector('#merge-filename').focus() - return - } - - // Disable merge button - e.target.disabled=true - e.target.classList.add('disabled') - let cursor = $("body").css("cursor"); - let label = document.querySelector('#merge-button').innerHTML - $("body").css("cursor", "progress"); - document.querySelector('#merge-button').innerHTML = 'Merging models ...' - - addLogMessage("Merging models") - addLogMessage("Model A: "+model0) - addLogMessage("Model B: "+model1) - - // Batch main loop - for (let i=0; i +
+
+ +
+
`, + onOpen: ({ firstOpen }) => { + if (!firstOpen) { + return } - addLogMessage(`merging batch job ${i+1}/${iterations}, alpha = ${alpha.toFixed(5)}...`) - request['out_path'] = document.querySelector('#merge-filename').value - request['out_path'] += '-' + alpha.toFixed(5) + '.' + document.querySelector('#merge-format').value - addLogMessage(`  filename: ${request['out_path']}`) + const tabSettingsSingle = document.querySelector('#tab-merge-opts-single') + const tabSettingsBatch = document.querySelector('#tab-merge-opts-batch') + linkTabContents(tabSettingsSingle) + linkTabContents(tabSettingsBatch) - request['ratio'] = alpha - let res = await fetch('/model/merge', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(request) }) - const data = await res.json(); - addLogMessage(JSON.stringify(data)) - } - addLogMessage("Done. The models have been saved to your models/stable-diffusion folder.") - addLogSeparator() - // Re-enable merge button - $("body").css("cursor", cursor); - document.querySelector('#merge-button').innerHTML = label - e.target.disabled=false - e.target.classList.remove('disabled') + console.log('Activate') + let mergeModelAField = new ModelDropdown(document.querySelector('#mergeModelA'), 'stable-diffusion') + let mergeModelBField = new ModelDropdown(document.querySelector('#mergeModelB'), 'stable-diffusion') + updateChart() - // Update model list - stableDiffusionModelField.innerHTML = '' - vaeModelField.innerHTML = '' - hypernetworkModelField.innerHTML = '' - await getModels() + // slider + const singleMergeRatioField = document.querySelector('#single-merge-ratio') + const singleMergeRatioSlider = document.querySelector('#single-merge-ratio-slider') + + function updateSingleMergeRatio() { + singleMergeRatioField.value = singleMergeRatioSlider.value / 10 + singleMergeRatioField.dispatchEvent(new Event("change")) + } + + function updateSingleMergeRatioSlider() { + if (singleMergeRatioField.value < 0) { + singleMergeRatioField.value = 0 + } else if (singleMergeRatioField.value > 100) { + singleMergeRatioField.value = 100 + } + + singleMergeRatioSlider.value = singleMergeRatioField.value * 10 + singleMergeRatioSlider.dispatchEvent(new Event("change")) + } + + singleMergeRatioSlider.addEventListener('input', updateSingleMergeRatio) + singleMergeRatioField.addEventListener('input', updateSingleMergeRatioSlider) + updateSingleMergeRatio() + + document.querySelector('.merge-config').addEventListener('change', updateChart) + + document.querySelector('#merge-button').addEventListener('click', async function(e) { + // Build request template + let model0 = mergeModelAField.value + let model1 = mergeModelBField.value + let request = { model0: model0, model1: model1 } + request['use_fp16'] = document.querySelector('#merge-fp').value == 'fp16' + let iterations = document.querySelector('#merge-count').value>>0 + let start = parseFloat( document.querySelector('#merge-start').value ) + let step = parseFloat( document.querySelector('#merge-step').value ) + + if (isTabActive(tabSettingsSingle)) { + start = parseFloat(singleMergeRatioField.value) + step = 0 + iterations = 1 + addLogMessage(`merge ratio = ${start}%`) + } else { + addLogMessage(`start = ${start}%`) + addLogMessage(`step = ${step}%`) + } + + if (start + (iterations-1) * step >= 100) { + addLogMessage('Aborting: maximum ratio is ≥ 100%') + addLogMessage('Reduce the number of variations or the step size') + addLogSeparator() + document.querySelector('#merge-count').focus() + return + } + + if (document.querySelector('#merge-filename').value == "") { + addLogMessage('Aborting: No output file name specified') + addLogSeparator() + document.querySelector('#merge-filename').focus() + return + } + + // Disable merge button + e.target.disabled=true + e.target.classList.add('disabled') + let cursor = $("body").css("cursor"); + let label = document.querySelector('#merge-button').innerHTML + $("body").css("cursor", "progress"); + document.querySelector('#merge-button').innerHTML = 'Merging models ...' + + addLogMessage("Merging models") + addLogMessage("Model A: "+model0) + addLogMessage("Model B: "+model1) + + // Batch main loop + for (let i=0; iDone. The models have been saved to your models/stable-diffusion folder.") + addLogSeparator() + // Re-enable merge button + $("body").css("cursor", cursor); + document.querySelector('#merge-button').innerHTML = label + e.target.disabled=false + e.target.classList.remove('disabled') + + // Update model list + stableDiffusionModelField.innerHTML = '' + vaeModelField.innerHTML = '' + hypernetworkModelField.innerHTML = '' + await getModels() + }) + }, }) })() diff --git a/ui/plugins/ui/release-notes.plugin.js b/ui/plugins/ui/release-notes.plugin.js index da7b79de..cb4db5e2 100644 --- a/ui/plugins/ui/release-notes.plugin.js +++ b/ui/plugins/ui/release-notes.plugin.js @@ -9,56 +9,41 @@ } } - document.querySelector('.tab-container')?.insertAdjacentHTML('beforeend', ` - - What's new? - - `) - - document.querySelector('#tab-content-wrapper')?.insertAdjacentHTML('beforeend', ` -
-
- Loading.. -
-
- `) - - const tabNews = document.querySelector('#tab-news') - if (tabNews) { - linkTabContents(tabNews) - } - const news = document.querySelector('#news') - if (!news) { - // news tab not found, dont exec plugin code. - return - } - - document.querySelector('body').insertAdjacentHTML('beforeend', ` - - `) + `, + onOpen: async ({ firstOpen }) => { + if (firstOpen) { + const loadMarkedScriptPromise = loadScript('/media/js/marked.min.js') - loadScript('/media/js/marked.min.js').then(async function() { - let appConfig = await fetch('/get/app_config') - if (!appConfig.ok) { - console.error('[release-notes] Failed to get app_config.') - return - } - appConfig = await appConfig.json() + let appConfig = await fetch('/get/app_config') + if (!appConfig.ok) { + console.error('[release-notes] Failed to get app_config.') + return + } + appConfig = await appConfig.json() + + const updateBranch = appConfig.update_branch || 'main' + + let releaseNotes = await fetch(`https://raw.githubusercontent.com/cmdr2/stable-diffusion-ui/${updateBranch}/CHANGES.md`) + if (!releaseNotes.ok) { + console.error('[release-notes] Failed to get CHANGES.md.') + return + } + releaseNotes = await releaseNotes.text() - const updateBranch = appConfig.update_branch || 'main' + await loadMarkedScriptPromise - let releaseNotes = await fetch(`https://raw.githubusercontent.com/cmdr2/stable-diffusion-ui/${updateBranch}/CHANGES.md`) - if (!releaseNotes.ok) { - console.error('[release-notes] Failed to get CHANGES.md.') - return - } - releaseNotes = await releaseNotes.text() - news.innerHTML = marked.parse(releaseNotes) + return marked.parse(releaseNotes) + } + }, }) })() \ No newline at end of file From 9c091a9edf01c98d4bdd1828ac0b8a614817418a Mon Sep 17 00:00:00 2001 From: Olivia Godone-Maresca Date: Thu, 6 Apr 2023 16:37:17 -0400 Subject: [PATCH 2/2] Fix JSDoc --- ui/media/js/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/media/js/utils.js b/ui/media/js/utils.js index 5599bae4..8421bfa0 100644 --- a/ui/media/js/utils.js +++ b/ui/media/js/utils.js @@ -711,7 +711,7 @@ function createElement(tagName, attributes, classes, textOrElements) { return element } -/* +/** * Add a listener for arrays * @param {keyof Array} method * @param {(args) => {}} callback