diff --git a/extensions-builtin/sdnext-kanvas b/extensions-builtin/sdnext-kanvas
index 83dbace79..86701f393 160000
--- a/extensions-builtin/sdnext-kanvas
+++ b/extensions-builtin/sdnext-kanvas
@@ -1 +1 @@
-Subproject commit 83dbace796d1a682b484f3499fc9475c4d27e00d
+Subproject commit 86701f3933e4f683719ad32de0420ba4a51f3a7c
diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui
index 753ee8d3e..183524444 160000
--- a/extensions-builtin/sdnext-modernui
+++ b/extensions-builtin/sdnext-modernui
@@ -1 +1 @@
-Subproject commit 753ee8d3efa5b7947adb19d9c19dcc8c3a5e7190
+Subproject commit 183524444e3c700775125e558db11db3625345fe
diff --git a/javascript/loader.js b/javascript/loader.js
index 597854fd1..3abd49a6a 100644
--- a/javascript/loader.js
+++ b/javascript/loader.js
@@ -1,4 +1,5 @@
const appStartTime = performance.now();
+let monitorLogActive = false;
async function preloadImages() {
const dark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
@@ -26,6 +27,39 @@ async function preloadImages() {
}
}
+function joinArgs(messages) {
+ let output = '';
+ for (let i = 0; i < messages.length; i++) {
+ let arg = messages[i];
+ if (arg === undefined) arg = 'undefined';
+ if (arg === null) arg = 'null';
+ output += ' ';
+ if (typeof arg === 'object') output += JSON.stringify(arg).replace(/["]+/g, '');
+ else output += arg;
+ }
+ return output;
+}
+
+async function monitorLog() {
+ if (window.logBufferDirty) {
+ window.logBufferDirty = false;
+ const maxLines = 5; // print last n logs from ring buffer to splash-log
+ const lines = [];
+ // print last 5 logs from ring buffer in reverse order
+ for (let i = window.logRingBuffer.length - 1; i >= Math.max(0, window.logRingBuffer.length - maxLines); i--) {
+ const logEntry = window.logRingBuffer[i];
+ let color = 'white';
+ if (logEntry.type === 'error') color = 'palevioletred';
+ else if (logEntry.type === 'debug') color = 'gray';
+ const html = `
${logEntry.ts} ${joinArgs(logEntry.msg)}
`;
+ lines.push(html);
+ }
+ const splashLogEl = document.getElementById('splashLog');
+ if (splashLogEl) splashLogEl.innerHTML = lines.join('');
+ }
+ if (monitorLogActive) setTimeout(monitorLog, 1000);
+}
+
async function removeSplash() {
const splash = document.getElementById('splash');
if (splash) splash.remove();
@@ -33,6 +67,7 @@ async function removeSplash() {
const t = Math.round(performance.now() - appStartTime);
log('startupTime', t);
xhrPost(`${window.api}/log`, { message: `ready time=${t}` });
+ monitorLogActive = false;
}
async function createSplash() {
@@ -43,6 +78,7 @@ async function createSplash() {
`;
document.body.insertAdjacentHTML('beforeend', splash);
const ok = await preloadImages();
@@ -51,14 +87,23 @@ async function createSplash() {
return;
}
const imgEl = ``;
- document.getElementById('splash').insertAdjacentHTML('afterbegin', imgEl);
- authFetch(`${window.api}/motd`)
+ const splashEl = document.getElementById('splash');
+ if (splashEl) splashEl.insertAdjacentHTML('afterbegin', imgEl);
+
+ monitorLogActive = true;
+ monitorLog();
+
+ await authFetch(`${window.api}/motd`)
.then((res) => res.text())
.then((text) => {
+ const clean = text.replace(/["]+/g, '');
+ log('getMOTD', clean);
const motdEl = document.getElementById('motd');
- if (motdEl) motdEl.innerHTML = text.replace(/["]+/g, '');
+ if (motdEl) motdEl.innerHTML = clean;
})
.catch((err) => error(`getMOTD: ${err}`));
+
+ log('loadGradioUi');
}
window.onload = createSplash;
diff --git a/javascript/logger.js b/javascript/logger.js
index 739afbb73..177c070ad 100644
--- a/javascript/logger.js
+++ b/javascript/logger.js
@@ -1,3 +1,12 @@
+window.logRingBuffer = [];
+window.logBufferDirty = false;
+
+const logBuffer = (ts, type, msg) => {
+ window.logRingBuffer.push({ ts, type, msg });
+ if (window.logRingBuffer.length > 10) window.logRingBuffer.shift();
+ window.logBufferDirty = true;
+};
+
const scrollBottom = async (el) => {
const lastChild = el.lastElementChild;
if (lastChild) lastChild.scrollIntoView({ behavior: 'smooth' });
@@ -11,6 +20,7 @@ const log = async (...msg) => {
scrollBottom(window.logger);
}
console.log(ts, ...msg);
+ logBuffer(ts, 'log', msg);
};
const debug = async (...msg) => {
@@ -21,6 +31,7 @@ const debug = async (...msg) => {
scrollBottom(window.logger);
}
console.debug(ts, ...msg);
+ logBuffer(ts, 'debug', msg);
};
const error = async (...msg) => {
@@ -31,6 +42,7 @@ const error = async (...msg) => {
scrollBottom(window.logger);
}
console.error(ts, ...msg);
+ logBuffer(ts, 'error', msg);
// const txt = msg.join(' ');
// if (!txt.includes('asctime') && !txt.includes('xhr.')) xhrPost('/sdapi/v1/log', { error: txt }); // eslint-disable-line no-use-before-define
};
diff --git a/javascript/script.js b/javascript/script.js
index 994880a45..399ada25f 100644
--- a/javascript/script.js
+++ b/javascript/script.js
@@ -67,7 +67,10 @@ function executeCallbacks(queue, arg) {
for (const callback of queue) {
if (!callback) continue;
try {
+ const t0 = performance.now();
callback(arg);
+ const t1 = performance.now();
+ if (t1 - t0 > 250) log('callbackSlow', callback.name || callback, `time=${Math.round(t1 - t0)}`);
} catch (e) {
error(`executeCallbacks: ${callback} ${e}`);
}
@@ -83,17 +86,21 @@ function scheduleAfterUiUpdateCallbacks() {
let executedOnLoaded = false;
const ignoreElements = ['logMonitorData', 'logWarnings', 'logErrors', 'tooltip-container', 'logger'];
+const ignoreElementsSet = new Set(ignoreElements);
const ignoreClasses = ['wrap'];
let mutationTimer = null;
let validMutations = [];
+
async function mutationCallback(mutations) {
- let newMutations = mutations;
- if (newMutations.length > 0) newMutations = newMutations.filter((m) => m.target.nodeName !== 'LABEL');
- if (newMutations.length > 0) newMutations = newMutations.filter((m) => ignoreElements.indexOf(m.target.id) === -1);
- if (newMutations.length > 0) newMutations = newMutations.filter((m) => m.target.id !== 'logWarnings' && m.target.id !== 'logErrors');
- if (newMutations.length > 0) newMutations = newMutations.filter((m) => !m.target.classList?.contains('wrap'));
- if (newMutations.length > 0) validMutations = validMutations.concat(newMutations);
+ if (mutations.length <= 0) return;
+ for (const mutation of mutations) {
+ const target = mutation.target;
+ if (target.nodeName === 'LABEL') continue;
+ if (ignoreElementsSet.has(target.id)) continue;
+ if (target.classList?.contains(ignoreClasses[0])) continue;
+ validMutations.push(mutation);
+ }
if (validMutations.length < 1) return;
if (mutationTimer) clearTimeout(mutationTimer);
@@ -113,12 +120,13 @@ async function mutationCallback(mutations) {
}
validMutations = [];
mutationTimer = null;
- }, 50);
+ }, 100);
}
document.addEventListener('DOMContentLoaded', () => {
+ log('DOMContentLoaded');
const mutationObserver = new MutationObserver(mutationCallback);
- mutationObserver.observe(gradioApp(), { childList: true, subtree: true });
+ mutationObserver.observe(gradioApp(), { childList: true, subtree: true, attributes: false });
});
/**
diff --git a/javascript/settings.js b/javascript/settings.js
index 9aa22c626..798e4ac82 100644
--- a/javascript/settings.js
+++ b/javascript/settings.js
@@ -15,7 +15,7 @@ function monitorOption(option, callback) {
monitoredOpts.push({ [option]: callback });
}
-const AppyOpts = [
+const AppyOpts = [ // monitored opts
{ compact_view: (val, old) => toggleCompact(val, old) },
{ gradio_theme: (val, old) => setTheme(val, old) },
{ font_size: (val, old) => setFontSize(val, old) },
@@ -38,7 +38,12 @@ async function updateOpts(json_string) {
for (const op of AppyOpts) {
const [key, callback] = Object.entries(op)[0];
- if (callback) callback(new_opts[key], opts[key]);
+ if (callback) {
+ const t3 = performance.now();
+ callback(new_opts[key], opts[key]);
+ const t4 = performance.now();
+ if (t4 - t3 > 100) debug('AppyOptSlow', key, `time=${Math.round(t4 - t3)}`);
+ }
}
const t2 = performance.now();
@@ -109,7 +114,7 @@ function updateAllOpts() {
return true;
}
-onAfterUiUpdate(async () => {
+async function onAfterUiUpdateCallback() {
if (!updateAllOpts()) return;
const json_elem = gradioApp().getElementById('settings_json');
const textarea = json_elem.querySelector('textarea');
@@ -146,15 +151,19 @@ onAfterUiUpdate(async () => {
});
}, 250);
};
-});
+}
-onOptionsChanged(() => {
+onAfterUiUpdate(onAfterUiUpdateCallback);
+
+async function onOptionsChangedCallback() {
const setting_elems = gradioApp().querySelectorAll('#settings [id^="setting_"]');
setting_elems.forEach((elem) => {
const setting_name = elem.id.replace('setting_', '');
markIfModified(setting_name, opts[setting_name]);
});
-});
+}
+
+onOptionsChanged(onOptionsChangedCallback);
async function initModels() {
const warn = () => `
diff --git a/javascript/startup.js b/javascript/startup.js
index a51a0272d..7168b0a9d 100644
--- a/javascript/startup.js
+++ b/javascript/startup.js
@@ -18,15 +18,15 @@ async function waitForOpts() {
break;
}
}
- await sleep(50);
+ await sleep(100);
t1 = performance.now();
}
}
async function initStartup() {
const t0 = performance.now();
- log('gradio', `time=${Math.round(t0 - appStartTime)}`);
- log('initStartup');
+ log('initGradio', `time=${Math.round(t0 - appStartTime)}`);
+ log('initUi');
if (window.setupLogger) await setupLogger();
// all items here are non-blocking async calls
@@ -54,22 +54,23 @@ async function initStartup() {
}
setRefreshInterval();
executeCallbacks(uiReadyCallbacks);
- initLogMonitor();
setupExtraNetworks();
// optinally wait for modern ui
if (window.waitForUiReady) await waitForUiReady();
initAutocomplete();
monitorConnection();
- removeSplash();
// post startup tasks that may take longer but are not critical
showNetworks();
setHints();
applyStyles();
initIndexDB();
+ initLogMonitor();
t1 = performance.now();
log('initStartup', Math.round(1000 * (t1 - t0) / 1000000));
+
+ removeSplash();
}
onUiLoaded(initStartup);
diff --git a/javascript/ui.js b/javascript/ui.js
index 913cb0fc6..9a939d282 100644
--- a/javascript/ui.js
+++ b/javascript/ui.js
@@ -4,6 +4,10 @@ window.titles = {};
let tabSelected = '';
let txt2img_textarea;
let img2img_textarea;
+let fontSizeApplyRaf = 0;
+let pendingFontSize = null;
+let appliedFontSize = null;
+let cachedGradioRoot = null;
const wait_time = 800;
const token_timeouts = {};
let uiLoaded = false;
@@ -132,18 +136,34 @@ async function setTheme(val, old) {
}
function setFontSize(val, old) {
- const size = val || opts.font_size;
- if (size === old) return;
- document.documentElement.style.setProperty('--font-size', `${size}px`);
- gradioApp().style.setProperty('--font-size', `${size}px`);
- gradioApp().style.setProperty('--text-xxs', `${size - 3}px`);
- gradioApp().style.setProperty('--text-xs', `${size - 2}px`);
- gradioApp().style.setProperty('--text-sm', `${size - 1}px`);
- gradioApp().style.setProperty('--text-md', `${size}px`);
- gradioApp().style.setProperty('--text-lg', `${size + 1}px`);
- gradioApp().style.setProperty('--text-xl', `${size + 2}px`);
- gradioApp().style.setProperty('--text-xxl', `${size + 3}px`);
- log('setFontSize', size);
+ const size = Number(val || opts.font_size);
+ if (!Number.isFinite(size)) return;
+ if (size === old || size === appliedFontSize || size === pendingFontSize) return;
+ pendingFontSize = size;
+ if (fontSizeApplyRaf) return;
+
+ fontSizeApplyRaf = requestAnimationFrame(() => {
+ const t0 = performance.now();
+ fontSizeApplyRaf = 0;
+ const nextSize = pendingFontSize;
+ pendingFontSize = null;
+ if (!Number.isFinite(nextSize) || nextSize === appliedFontSize) return;
+
+ cachedGradioRoot = cachedGradioRoot || gradioApp();
+ const rootStyle = cachedGradioRoot.style;
+ document.documentElement.style.setProperty('--font-size', `${nextSize}px`);
+ rootStyle.setProperty('--font-size', `${nextSize}px`);
+ rootStyle.setProperty('--text-xxs', `${nextSize - 3}px`);
+ rootStyle.setProperty('--text-xs', `${nextSize - 2}px`);
+ rootStyle.setProperty('--text-sm', `${nextSize - 1}px`);
+ rootStyle.setProperty('--text-md', `${nextSize}px`);
+ rootStyle.setProperty('--text-lg', `${nextSize + 1}px`);
+ rootStyle.setProperty('--text-xl', `${nextSize + 2}px`);
+ rootStyle.setProperty('--text-xxl', `${nextSize + 3}px`);
+ appliedFontSize = nextSize;
+ const t1 = performance.now();
+ log('setFontSize', nextSize, `time=${Math.round(t1 - t0)}`);
+ });
}
function switchToTab(tab) {
@@ -350,6 +370,28 @@ function clearPrompts(prompt, negative_prompt) {
}
const promptTokenCountUpdateFuncs = {};
+const registeredPromptTextareas = new WeakSet();
+const registeredPromptIds = new Set();
+const pendingCounterPlacement = new Set();
+const promptRegistrationConfig = [
+ ['txt2img_prompt', 'txt2img_token_counter', 'txt2img_token_button'],
+ ['txt2img_neg_prompt', 'txt2img_negative_token_counter', 'txt2img_negative_token_button'],
+ ['img2img_prompt', 'img2img_token_counter', 'img2img_token_button'],
+ ['img2img_neg_prompt', 'img2img_negative_token_counter', 'img2img_negative_token_button'],
+ ['control_prompt', 'control_token_counter', 'control_token_button'],
+ ['control_neg_prompt', 'control_negative_token_counter', 'control_negative_token_button'],
+];
+let promptRegistrationRaf = 0;
+let promptRegistrationCursor = 0;
+let promptRegistrationInProgress = false;
+
+function scheduleIdleUI(task) {
+ if (typeof window.requestIdleCallback === 'function') {
+ window.requestIdleCallback(task, { timeout: 500 });
+ } else {
+ setTimeout(task, 0);
+ }
+}
function recalculatePromptTokens(name) {
if (promptTokenCountUpdateFuncs[name]) {
@@ -427,30 +469,89 @@ function sortUIElements() {
log('sortUIElements');
}
-onAfterUiUpdate(async () => {
- async function registerTextarea(id, id_counter, id_button) {
- const prompt = gradioApp().getElementById(id);
- if (!prompt) return;
- const counter = gradioApp().getElementById(id_counter);
- const localTextarea = gradioApp().querySelector(`#${id} > label > textarea`);
- if (counter.parentElement === prompt.parentElement) return;
- prompt.parentElement.insertBefore(counter, prompt);
- prompt.parentElement.style.position = 'relative';
- promptTokenCountUpdateFuncs[id] = () => { update_token_counter(id_button); };
- localTextarea.addEventListener('input', promptTokenCountUpdateFuncs[id]);
- }
-
+function registerTextareaCallback() {
// sortUIElements();
if (promptsInitialized) return;
- log('initPrompts');
- registerTextarea('txt2img_prompt', 'txt2img_token_counter', 'txt2img_token_button');
- registerTextarea('txt2img_neg_prompt', 'txt2img_negative_token_counter', 'txt2img_negative_token_button');
- registerTextarea('img2img_prompt', 'img2img_token_counter', 'img2img_token_button');
- registerTextarea('img2img_neg_prompt', 'img2img_negative_token_counter', 'img2img_negative_token_button');
- registerTextarea('control_prompt', 'control_token_counter', 'control_token_button');
- registerTextarea('control_neg_prompt', 'control_negative_token_counter', 'control_negative_token_button');
- promptsInitialized = true;
-});
+ if (promptRegistrationInProgress) return;
+
+ const app = gradioApp();
+ if (!app) return;
+
+ const registerTextarea = (id, id_counter, id_button) => {
+ const prompt = app.getElementById(id);
+ const counter = app.getElementById(id_counter);
+ const localTextarea = prompt?.querySelector('label > textarea');
+ if (!prompt || !counter || !localTextarea || !prompt.parentElement) return false;
+
+ const promptParent = prompt.parentElement;
+ const needsCounterPlacement = counter.parentElement !== promptParent || counter.nextElementSibling !== prompt;
+ if (needsCounterPlacement && !pendingCounterPlacement.has(id)) {
+ pendingCounterPlacement.add(id);
+ scheduleIdleUI(() => {
+ pendingCounterPlacement.delete(id);
+ const currentPrompt = app.getElementById(id);
+ const currentCounter = app.getElementById(id_counter);
+ if (!currentPrompt || !currentCounter || !currentPrompt.parentElement) return;
+ const currentParent = currentPrompt.parentElement;
+ if (currentCounter.parentElement !== currentParent || currentCounter.nextElementSibling !== currentPrompt) {
+ currentParent.insertBefore(currentCounter, currentPrompt);
+ }
+ if (currentParent.style.position !== 'relative') {
+ currentParent.style.position = 'relative';
+ }
+ });
+ }
+
+ if (!promptTokenCountUpdateFuncs[id]) promptTokenCountUpdateFuncs[id] = () => { update_token_counter(id_button); };
+ if (!registeredPromptTextareas.has(localTextarea)) {
+ localTextarea.addEventListener('input', promptTokenCountUpdateFuncs[id]);
+ registeredPromptTextareas.add(localTextarea);
+ }
+ return true;
+ };
+
+ const runPromptRegistrationStep = () => {
+ promptRegistrationRaf = 0;
+ const total = promptRegistrationConfig.length;
+ let cfg = null;
+
+ // Process one prompt registration per frame to avoid long blocking work.
+ for (let attempts = 0; attempts < total; attempts += 1) {
+ const nextCfg = promptRegistrationConfig[promptRegistrationCursor];
+ promptRegistrationCursor = (promptRegistrationCursor + 1) % total;
+ const [id] = nextCfg;
+ if (!registeredPromptIds.has(id)) {
+ cfg = nextCfg;
+ break;
+ }
+ }
+
+ if (cfg) {
+ const [id] = cfg;
+ if (registerTextarea(...cfg)) {
+ registeredPromptIds.add(id);
+ } else {
+ // Prompt not available yet, retry on next onAfterUiUpdate callback.
+ promptRegistrationInProgress = false;
+ return;
+ }
+ }
+
+ promptsInitialized = registeredPromptIds.size === total;
+ if (promptsInitialized) {
+ promptRegistrationInProgress = false;
+ log('initPrompts', registeredPromptIds.size);
+ return;
+ }
+
+ promptRegistrationRaf = requestAnimationFrame(runPromptRegistrationStep);
+ };
+
+ promptRegistrationInProgress = true;
+ promptRegistrationRaf = requestAnimationFrame(runPromptRegistrationStep);
+}
+
+onAfterUiUpdate(registerTextareaCallback);
function update_txt2img_tokens(...args) {
update_token_counter('txt2img_token_button');
@@ -564,7 +665,7 @@ function createThemeElement() {
return el;
}
-function toggleCompact(val, old) {
+async function toggleCompact(val, old) {
if (val === old) return;
log('toggleCompact', val, old);
if (val) {
diff --git a/modules/ui_definitions.py b/modules/ui_definitions.py
index 4b573296d..e01eb72be 100644
--- a/modules/ui_definitions.py
+++ b/modules/ui_definitions.py
@@ -526,6 +526,23 @@ def create_settings(cmd_opts):
"compact_view": OptionInfo(False, "Compact view"),
"ui_columns": OptionInfo(4, "Gallery view columns", gr.Slider, {"minimum": 1, "maximum": 8, "step": 1}),
+ 'uiux_separator_appearance': OptionInfo("Appearance
", "", gr.HTML),
+ "uiux_grid_image_size": OptionInfo(150, "Grid image size", gr.Slider, {"minimum": 64, "maximum": 1024, "step": 1}),
+ "uiux_panel_min_width": OptionInfo(35, "Panel minimum width", gr.Number),
+ "uiux_hide_legacy": OptionInfo(True, "Hide legacy tabs"),
+ "uiux_persist_layout": OptionInfo(True, "Persist UI layout"),
+ "uiux_no_slider_layout": OptionInfo(False, "Hide input range sliders"),
+ "uiux_show_labels_aside": OptionInfo(False, "Show labels for aside tabs"),
+ "uiux_show_labels_main": OptionInfo(False, "Show labels for main tabs"),
+ "uiux_show_labels_tabs": OptionInfo(True, "Show labels for page tabs"),
+ "uiux_show_input_range_ticks": OptionInfo(True, "Show ticks for input range slider", gr.Checkbox, {"visible": False}),
+ "uiux_no_headers_params": OptionInfo(False, "Hide params headers", gr.Checkbox, {"visible": False}),
+ "uiux_show_outline_params": OptionInfo(True, "Show parameter outline", gr.Checkbox, {"visible": False}),
+
+ 'uiux_separator_mobile': OptionInfo("Mobile
", "", gr.HTML),
+ "uiux_default_layout": OptionInfo("Auto", "Layout", gr.Radio, {"choices": ["Auto","Desktop", "Mobile"]}),
+ "uiux_mobile_scale": OptionInfo(0.7, "Mobile scale", gr.Slider, {"minimum": 0.5, "maximum": 1, "step": 0.05}),
+
"images_sep_log": OptionInfo("Log Display
", "", gr.HTML),
"logmonitor_show": OptionInfo(True, "Show log view"),
"logmonitor_refresh_period": OptionInfo(5000, "Log view update period", gr.Slider, {"minimum": 0, "maximum": 30000, "step": 25}),