Path: blob/master/javascript/extraNetworks.js
3055 views
function toggleCss(key, css, enable) {1var style = document.getElementById(key);2if (enable && !style) {3style = document.createElement('style');4style.id = key;5style.type = 'text/css';6document.head.appendChild(style);7}8if (style && !enable) {9document.head.removeChild(style);10}11if (style) {12style.innerHTML == '';13style.appendChild(document.createTextNode(css));14}15}1617function setupExtraNetworksForTab(tabname) {18function registerPrompt(tabname, id) {19var textarea = gradioApp().querySelector("#" + id + " > label > textarea");2021if (!activePromptTextarea[tabname]) {22activePromptTextarea[tabname] = textarea;23}2425textarea.addEventListener("focus", function() {26activePromptTextarea[tabname] = textarea;27});28}2930var tabnav = gradioApp().querySelector('#' + tabname + '_extra_tabs > div.tab-nav');31var controlsDiv = document.createElement('DIV');32controlsDiv.classList.add('extra-networks-controls-div');33tabnav.appendChild(controlsDiv);34tabnav.insertBefore(controlsDiv, null);3536var this_tab = gradioApp().querySelector('#' + tabname + '_extra_tabs');37this_tab.querySelectorAll(":scope > [id^='" + tabname + "_']").forEach(function(elem) {38// tabname_full = {tabname}_{extra_networks_tabname}39var tabname_full = elem.id;40var search = gradioApp().querySelector("#" + tabname_full + "_extra_search");41var sort_dir = gradioApp().querySelector("#" + tabname_full + "_extra_sort_dir");42var refresh = gradioApp().querySelector("#" + tabname_full + "_extra_refresh");43var currentSort = '';4445// If any of the buttons above don't exist, we want to skip this iteration of the loop.46if (!search || !sort_dir || !refresh) {47return; // `return` is equivalent of `continue` but for forEach loops.48}4950var applyFilter = function(force) {51var searchTerm = search.value.toLowerCase();52gradioApp().querySelectorAll('#' + tabname + '_extra_tabs div.card').forEach(function(elem) {53var searchOnly = elem.querySelector('.search_only');54var text = Array.prototype.map.call(elem.querySelectorAll('.search_terms, .description'), function(t) {55return t.textContent.toLowerCase();56}).join(" ");5758var visible = text.indexOf(searchTerm) != -1;59if (searchOnly && searchTerm.length < 4) {60visible = false;61}62if (visible) {63elem.classList.remove("hidden");64} else {65elem.classList.add("hidden");66}67});6869applySort(force);70};7172var applySort = function(force) {73var cards = gradioApp().querySelectorAll('#' + tabname_full + ' div.card');74var parent = gradioApp().querySelector('#' + tabname_full + "_cards");75var reverse = sort_dir.dataset.sortdir == "Descending";76var activeSearchElem = gradioApp().querySelector('#' + tabname_full + "_controls .extra-network-control--sort.extra-network-control--enabled");77var sortKey = activeSearchElem ? activeSearchElem.dataset.sortkey : "default";78var sortKeyDataField = "sort" + sortKey.charAt(0).toUpperCase() + sortKey.slice(1);79var sortKeyStore = sortKey + "-" + sort_dir.dataset.sortdir + "-" + cards.length;8081if (sortKeyStore == currentSort && !force) {82return;83}84currentSort = sortKeyStore;8586var sortedCards = Array.from(cards);87sortedCards.sort(function(cardA, cardB) {88var a = cardA.dataset[sortKeyDataField];89var b = cardB.dataset[sortKeyDataField];90if (!isNaN(a) && !isNaN(b)) {91return parseInt(a) - parseInt(b);92}9394return (a < b ? -1 : (a > b ? 1 : 0));95});9697if (reverse) {98sortedCards.reverse();99}100101parent.innerHTML = '';102103var frag = document.createDocumentFragment();104sortedCards.forEach(function(card) {105frag.appendChild(card);106});107parent.appendChild(frag);108};109110search.addEventListener("input", function() {111applyFilter();112});113applySort();114applyFilter();115extraNetworksApplySort[tabname_full] = applySort;116extraNetworksApplyFilter[tabname_full] = applyFilter;117118var controls = gradioApp().querySelector("#" + tabname_full + "_controls");119controlsDiv.insertBefore(controls, null);120121if (elem.style.display != "none") {122extraNetworksShowControlsForPage(tabname, tabname_full);123}124});125126registerPrompt(tabname, tabname + "_prompt");127registerPrompt(tabname, tabname + "_neg_prompt");128}129130function extraNetworksMovePromptToTab(tabname, id, showPrompt, showNegativePrompt) {131if (!gradioApp().querySelector('.toprow-compact-tools')) return; // only applicable for compact prompt layout132133var promptContainer = gradioApp().getElementById(tabname + '_prompt_container');134var prompt = gradioApp().getElementById(tabname + '_prompt_row');135var negPrompt = gradioApp().getElementById(tabname + '_neg_prompt_row');136var elem = id ? gradioApp().getElementById(id) : null;137138if (showNegativePrompt && elem) {139elem.insertBefore(negPrompt, elem.firstChild);140} else {141promptContainer.insertBefore(negPrompt, promptContainer.firstChild);142}143144if (showPrompt && elem) {145elem.insertBefore(prompt, elem.firstChild);146} else {147promptContainer.insertBefore(prompt, promptContainer.firstChild);148}149150if (elem) {151elem.classList.toggle('extra-page-prompts-active', showNegativePrompt || showPrompt);152}153}154155156function extraNetworksShowControlsForPage(tabname, tabname_full) {157gradioApp().querySelectorAll('#' + tabname + '_extra_tabs .extra-networks-controls-div > div').forEach(function(elem) {158var targetId = tabname_full + "_controls";159elem.style.display = elem.id == targetId ? "" : "none";160});161}162163164function extraNetworksUnrelatedTabSelected(tabname) { // called from python when user selects an unrelated tab (generate)165extraNetworksMovePromptToTab(tabname, '', false, false);166167extraNetworksShowControlsForPage(tabname, null);168}169170function extraNetworksTabSelected(tabname, id, showPrompt, showNegativePrompt, tabname_full) { // called from python when user selects an extra networks tab171extraNetworksMovePromptToTab(tabname, id, showPrompt, showNegativePrompt);172173extraNetworksShowControlsForPage(tabname, tabname_full);174}175176function applyExtraNetworkFilter(tabname_full) {177var doFilter = function() {178var applyFunction = extraNetworksApplyFilter[tabname_full];179180if (applyFunction) {181applyFunction(true);182}183};184setTimeout(doFilter, 1);185}186187function applyExtraNetworkSort(tabname_full) {188var doSort = function() {189extraNetworksApplySort[tabname_full](true);190};191setTimeout(doSort, 1);192}193194var extraNetworksApplyFilter = {};195var extraNetworksApplySort = {};196var activePromptTextarea = {};197198function setupExtraNetworks() {199setupExtraNetworksForTab('txt2img');200setupExtraNetworksForTab('img2img');201}202203var re_extranet = /<([^:^>]+:[^:]+):[\d.]+>(.*)/;204var re_extranet_g = /<([^:^>]+:[^:]+):[\d.]+>/g;205206var re_extranet_neg = /\(([^:^>]+:[\d.]+)\)/;207var re_extranet_g_neg = /\(([^:^>]+:[\d.]+)\)/g;208function tryToRemoveExtraNetworkFromPrompt(textarea, text, isNeg) {209var m = text.match(isNeg ? re_extranet_neg : re_extranet);210var replaced = false;211var newTextareaText;212var extraTextBeforeNet = opts.extra_networks_add_text_separator;213if (m) {214var extraTextAfterNet = m[2];215var partToSearch = m[1];216var foundAtPosition = -1;217newTextareaText = textarea.value.replaceAll(isNeg ? re_extranet_g_neg : re_extranet_g, function(found, net, pos) {218m = found.match(isNeg ? re_extranet_neg : re_extranet);219if (m[1] == partToSearch) {220replaced = true;221foundAtPosition = pos;222return "";223}224return found;225});226if (foundAtPosition >= 0) {227if (extraTextAfterNet && newTextareaText.substr(foundAtPosition, extraTextAfterNet.length) == extraTextAfterNet) {228newTextareaText = newTextareaText.substr(0, foundAtPosition) + newTextareaText.substr(foundAtPosition + extraTextAfterNet.length);229}230if (newTextareaText.substr(foundAtPosition - extraTextBeforeNet.length, extraTextBeforeNet.length) == extraTextBeforeNet) {231newTextareaText = newTextareaText.substr(0, foundAtPosition - extraTextBeforeNet.length) + newTextareaText.substr(foundAtPosition);232}233}234} else {235newTextareaText = textarea.value.replaceAll(new RegExp(`((?:${extraTextBeforeNet})?${text})`, "g"), "");236replaced = (newTextareaText != textarea.value);237}238239if (replaced) {240textarea.value = newTextareaText;241return true;242}243244return false;245}246247function updatePromptArea(text, textArea, isNeg) {248if (!tryToRemoveExtraNetworkFromPrompt(textArea, text, isNeg)) {249textArea.value = textArea.value + opts.extra_networks_add_text_separator + text;250}251252updateInput(textArea);253}254255function cardClicked(tabname, textToAdd, textToAddNegative, allowNegativePrompt) {256if (textToAddNegative.length > 0) {257updatePromptArea(textToAdd, gradioApp().querySelector("#" + tabname + "_prompt > label > textarea"));258updatePromptArea(textToAddNegative, gradioApp().querySelector("#" + tabname + "_neg_prompt > label > textarea"), true);259} else {260var textarea = allowNegativePrompt ? activePromptTextarea[tabname] : gradioApp().querySelector("#" + tabname + "_prompt > label > textarea");261updatePromptArea(textToAdd, textarea);262}263}264265function saveCardPreview(event, tabname, filename) {266var textarea = gradioApp().querySelector("#" + tabname + '_preview_filename > label > textarea');267var button = gradioApp().getElementById(tabname + '_save_preview');268269textarea.value = filename;270updateInput(textarea);271272button.click();273274event.stopPropagation();275event.preventDefault();276}277278function extraNetworksSearchButton(tabname, extra_networks_tabname, event) {279var searchTextarea = gradioApp().querySelector("#" + tabname + "_" + extra_networks_tabname + "_extra_search");280var button = event.target;281var text = button.classList.contains("search-all") ? "" : button.textContent.trim();282283searchTextarea.value = text;284updateInput(searchTextarea);285}286287function extraNetworksTreeProcessFileClick(event, btn, tabname, extra_networks_tabname) {288/**289* Processes `onclick` events when user clicks on files in tree.290*291* @param event The generated event.292* @param btn The clicked `tree-list-item` button.293* @param tabname The name of the active tab in the sd webui. Ex: txt2img, img2img, etc.294* @param extra_networks_tabname The id of the active extraNetworks tab. Ex: lora, checkpoints, etc.295*/296// NOTE: Currently unused.297return;298}299300function extraNetworksTreeProcessDirectoryClick(event, btn, tabname, extra_networks_tabname) {301/**302* Processes `onclick` events when user clicks on directories in tree.303*304* Here is how the tree reacts to clicks for various states:305* unselected unopened directory: Directory is selected and expanded.306* unselected opened directory: Directory is selected.307* selected opened directory: Directory is collapsed and deselected.308* chevron is clicked: Directory is expanded or collapsed. Selected state unchanged.309*310* @param event The generated event.311* @param btn The clicked `tree-list-item` button.312* @param tabname The name of the active tab in the sd webui. Ex: txt2img, img2img, etc.313* @param extra_networks_tabname The id of the active extraNetworks tab. Ex: lora, checkpoints, etc.314*/315var ul = btn.nextElementSibling;316// This is the actual target that the user clicked on within the target button.317// We use this to detect if the chevron was clicked.318var true_targ = event.target;319320function _expand_or_collapse(_ul, _btn) {321// Expands <ul> if it is collapsed, collapses otherwise. Updates button attributes.322if (_ul.hasAttribute("hidden")) {323_ul.removeAttribute("hidden");324_btn.dataset.expanded = "";325} else {326_ul.setAttribute("hidden", "");327delete _btn.dataset.expanded;328}329}330331function _remove_selected_from_all() {332// Removes the `selected` attribute from all buttons.333var sels = document.querySelectorAll("div.tree-list-content");334[...sels].forEach(el => {335delete el.dataset.selected;336});337}338339function _select_button(_btn) {340// Removes `data-selected` attribute from all buttons then adds to passed button.341_remove_selected_from_all();342_btn.dataset.selected = "";343}344345function _update_search(_tabname, _extra_networks_tabname, _search_text) {346// Update search input with select button's path.347var search_input_elem = gradioApp().querySelector("#" + tabname + "_" + extra_networks_tabname + "_extra_search");348search_input_elem.value = _search_text;349updateInput(search_input_elem);350}351352353// If user clicks on the chevron, then we do not select the folder.354if (true_targ.matches(".tree-list-item-action--leading, .tree-list-item-action-chevron")) {355_expand_or_collapse(ul, btn);356} else {357// User clicked anywhere else on the button.358if ("selected" in btn.dataset && !(ul.hasAttribute("hidden"))) {359// If folder is select and open, collapse and deselect button.360_expand_or_collapse(ul, btn);361delete btn.dataset.selected;362_update_search(tabname, extra_networks_tabname, "");363} else if (!(!("selected" in btn.dataset) && !(ul.hasAttribute("hidden")))) {364// If folder is open and not selected, then we don't collapse; just select.365// NOTE: Double inversion sucks but it is the clearest way to show the branching here.366_expand_or_collapse(ul, btn);367_select_button(btn, tabname, extra_networks_tabname);368_update_search(tabname, extra_networks_tabname, btn.dataset.path);369} else {370// All other cases, just select the button.371_select_button(btn, tabname, extra_networks_tabname);372_update_search(tabname, extra_networks_tabname, btn.dataset.path);373}374}375}376377function extraNetworksTreeOnClick(event, tabname, extra_networks_tabname) {378/**379* Handles `onclick` events for buttons within an `extra-network-tree .tree-list--tree`.380*381* Determines whether the clicked button in the tree is for a file entry or a directory382* then calls the appropriate function.383*384* @param event The generated event.385* @param tabname The name of the active tab in the sd webui. Ex: txt2img, img2img, etc.386* @param extra_networks_tabname The id of the active extraNetworks tab. Ex: lora, checkpoints, etc.387*/388var btn = event.currentTarget;389var par = btn.parentElement;390if (par.dataset.treeEntryType === "file") {391extraNetworksTreeProcessFileClick(event, btn, tabname, extra_networks_tabname);392} else {393extraNetworksTreeProcessDirectoryClick(event, btn, tabname, extra_networks_tabname);394}395}396397function extraNetworksControlSortOnClick(event, tabname, extra_networks_tabname) {398/** Handles `onclick` events for Sort Mode buttons. */399400var self = event.currentTarget;401var parent = event.currentTarget.parentElement;402403parent.querySelectorAll('.extra-network-control--sort').forEach(function(x) {404x.classList.remove('extra-network-control--enabled');405});406407self.classList.add('extra-network-control--enabled');408409applyExtraNetworkSort(tabname + "_" + extra_networks_tabname);410}411412function extraNetworksControlSortDirOnClick(event, tabname, extra_networks_tabname) {413/**414* Handles `onclick` events for the Sort Direction button.415*416* Modifies the data attributes of the Sort Direction button to cycle between417* ascending and descending sort directions.418*419* @param event The generated event.420* @param tabname The name of the active tab in the sd webui. Ex: txt2img, img2img, etc.421* @param extra_networks_tabname The id of the active extraNetworks tab. Ex: lora, checkpoints, etc.422*/423if (event.currentTarget.dataset.sortdir == "Ascending") {424event.currentTarget.dataset.sortdir = "Descending";425event.currentTarget.setAttribute("title", "Sort descending");426} else {427event.currentTarget.dataset.sortdir = "Ascending";428event.currentTarget.setAttribute("title", "Sort ascending");429}430applyExtraNetworkSort(tabname + "_" + extra_networks_tabname);431}432433function extraNetworksControlTreeViewOnClick(event, tabname, extra_networks_tabname) {434/**435* Handles `onclick` events for the Tree View button.436*437* Toggles the tree view in the extra networks pane.438*439* @param event The generated event.440* @param tabname The name of the active tab in the sd webui. Ex: txt2img, img2img, etc.441* @param extra_networks_tabname The id of the active extraNetworks tab. Ex: lora, checkpoints, etc.442*/443var button = event.currentTarget;444button.classList.toggle("extra-network-control--enabled");445var show = !button.classList.contains("extra-network-control--enabled");446447var pane = gradioApp().getElementById(tabname + "_" + extra_networks_tabname + "_pane");448pane.classList.toggle("extra-network-dirs-hidden", show);449}450451function extraNetworksControlRefreshOnClick(event, tabname, extra_networks_tabname) {452/**453* Handles `onclick` events for the Refresh Page button.454*455* In order to actually call the python functions in `ui_extra_networks.py`456* to refresh the page, we created an empty gradio button in that file with an457* event handler that refreshes the page. So what this function here does458* is it manually raises a `click` event on that button.459*460* @param event The generated event.461* @param tabname The name of the active tab in the sd webui. Ex: txt2img, img2img, etc.462* @param extra_networks_tabname The id of the active extraNetworks tab. Ex: lora, checkpoints, etc.463*/464var btn_refresh_internal = gradioApp().getElementById(tabname + "_" + extra_networks_tabname + "_extra_refresh_internal");465btn_refresh_internal.dispatchEvent(new Event("click"));466}467468var globalPopup = null;469var globalPopupInner = null;470471function closePopup() {472if (!globalPopup) return;473globalPopup.style.display = "none";474}475476function popup(contents) {477if (!globalPopup) {478globalPopup = document.createElement('div');479globalPopup.classList.add('global-popup');480481var close = document.createElement('div');482close.classList.add('global-popup-close');483close.addEventListener("click", closePopup);484close.title = "Close";485globalPopup.appendChild(close);486487globalPopupInner = document.createElement('div');488globalPopupInner.classList.add('global-popup-inner');489globalPopup.appendChild(globalPopupInner);490491gradioApp().querySelector('.main').appendChild(globalPopup);492}493494globalPopupInner.innerHTML = '';495globalPopupInner.appendChild(contents);496497globalPopup.style.display = "flex";498}499500var storedPopupIds = {};501function popupId(id) {502if (!storedPopupIds[id]) {503storedPopupIds[id] = gradioApp().getElementById(id);504}505506popup(storedPopupIds[id]);507}508509function extraNetworksFlattenMetadata(obj) {510const result = {};511512// Convert any stringified JSON objects to actual objects513for (const key of Object.keys(obj)) {514if (typeof obj[key] === 'string') {515try {516const parsed = JSON.parse(obj[key]);517if (parsed && typeof parsed === 'object') {518obj[key] = parsed;519}520} catch (error) {521continue;522}523}524}525526// Flatten the object527for (const key of Object.keys(obj)) {528if (typeof obj[key] === 'object' && obj[key] !== null) {529const nested = extraNetworksFlattenMetadata(obj[key]);530for (const nestedKey of Object.keys(nested)) {531result[`${key}/${nestedKey}`] = nested[nestedKey];532}533} else {534result[key] = obj[key];535}536}537538// Special case for handling modelspec keys539for (const key of Object.keys(result)) {540if (key.startsWith("modelspec.")) {541result[key.replaceAll(".", "/")] = result[key];542delete result[key];543}544}545546// Add empty keys to designate hierarchy547for (const key of Object.keys(result)) {548const parts = key.split("/");549for (let i = 1; i < parts.length; i++) {550const parent = parts.slice(0, i).join("/");551if (!result[parent]) {552result[parent] = "";553}554}555}556557return result;558}559560function extraNetworksShowMetadata(text) {561try {562let parsed = JSON.parse(text);563if (parsed && typeof parsed === 'object') {564parsed = extraNetworksFlattenMetadata(parsed);565const table = createVisualizationTable(parsed, 0);566popup(table);567return;568}569} catch (error) {570console.error(error);571}572573var elem = document.createElement('pre');574elem.classList.add('popup-metadata');575elem.textContent = text;576577popup(elem);578return;579}580581function requestGet(url, data, handler, errorHandler) {582var xhr = new XMLHttpRequest();583var args = Object.keys(data).map(function(k) {584return encodeURIComponent(k) + '=' + encodeURIComponent(data[k]);585}).join('&');586xhr.open("GET", url + "?" + args, true);587588xhr.onreadystatechange = function() {589if (xhr.readyState === 4) {590if (xhr.status === 200) {591try {592var js = JSON.parse(xhr.responseText);593handler(js);594} catch (error) {595console.error(error);596errorHandler();597}598} else {599errorHandler();600}601}602};603var js = JSON.stringify(data);604xhr.send(js);605}606607function extraNetworksCopyCardPath(event) {608navigator.clipboard.writeText(event.target.getAttribute("data-clipboard-text"));609event.stopPropagation();610}611612function extraNetworksRequestMetadata(event, extraPage) {613var showError = function() {614extraNetworksShowMetadata("there was an error getting metadata");615};616617var cardName = event.target.parentElement.parentElement.getAttribute("data-name");618619requestGet("./sd_extra_networks/metadata", {page: extraPage, item: cardName}, function(data) {620if (data && data.metadata) {621extraNetworksShowMetadata(data.metadata);622} else {623showError();624}625}, showError);626627event.stopPropagation();628}629630var extraPageUserMetadataEditors = {};631632function extraNetworksEditUserMetadata(event, tabname, extraPage) {633var id = tabname + '_' + extraPage + '_edit_user_metadata';634635var editor = extraPageUserMetadataEditors[id];636if (!editor) {637editor = {};638editor.page = gradioApp().getElementById(id);639editor.nameTextarea = gradioApp().querySelector("#" + id + "_name" + ' textarea');640editor.button = gradioApp().querySelector("#" + id + "_button");641extraPageUserMetadataEditors[id] = editor;642}643644var cardName = event.target.parentElement.parentElement.getAttribute("data-name");645editor.nameTextarea.value = cardName;646updateInput(editor.nameTextarea);647648editor.button.click();649650popup(editor.page);651652event.stopPropagation();653}654655function extraNetworksRefreshSingleCard(page, tabname, name) {656requestGet("./sd_extra_networks/get-single-card", {page: page, tabname: tabname, name: name}, function(data) {657if (data && data.html) {658var card = gradioApp().querySelector(`#${tabname}_${page.replace(" ", "_")}_cards > .card[data-name="${name}"]`);659660var newDiv = document.createElement('DIV');661newDiv.innerHTML = data.html;662var newCard = newDiv.firstElementChild;663664newCard.style.display = '';665card.parentElement.insertBefore(newCard, card);666card.parentElement.removeChild(card);667}668});669}670671window.addEventListener("keydown", function(event) {672if (event.key == "Escape") {673closePopup();674}675});676677/**678* Setup custom loading for this script.679* We need to wait for all of our HTML to be generated in the extra networks tabs680* before we can actually run the `setupExtraNetworks` function.681* The `onUiLoaded` function actually runs before all of our extra network tabs are682* finished generating. Thus we needed this new method.683*684*/685686var uiAfterScriptsCallbacks = [];687var uiAfterScriptsTimeout = null;688var executedAfterScripts = false;689690function scheduleAfterScriptsCallbacks() {691clearTimeout(uiAfterScriptsTimeout);692uiAfterScriptsTimeout = setTimeout(function() {693executeCallbacks(uiAfterScriptsCallbacks);694}, 200);695}696697onUiLoaded(function() {698var mutationObserver = new MutationObserver(function(m) {699let existingSearchfields = gradioApp().querySelectorAll("[id$='_extra_search']").length;700let neededSearchfields = gradioApp().querySelectorAll("[id$='_extra_tabs'] > .tab-nav > button").length - 2;701702if (!executedAfterScripts && existingSearchfields >= neededSearchfields) {703mutationObserver.disconnect();704executedAfterScripts = true;705scheduleAfterScriptsCallbacks();706}707});708mutationObserver.observe(gradioApp(), {childList: true, subtree: true});709});710711uiAfterScriptsCallbacks.push(setupExtraNetworks);712713714