diff --git a/__init__.py b/__init__.py index b98f544..f0c91b9 100644 --- a/__init__.py +++ b/__init__.py @@ -1,5 +1,6 @@ import os import pathlib +from datetime import datetime import sys import copy import importlib @@ -68,6 +69,29 @@ def folder_paths_get_supported_pt_extensions(folder_name, refresh = False): # Mi return model_extensions +def search_path_to_system_path(model_path, model_path_type): + # TODO: return model type (since it is bakedi into the search path anyways; simplifies other code) + model_path = model_path.replace("/", os.path.sep) + regex_result = re.search(r'\d+', model_path) + if regex_result is None: + return None + try: + model_path_index = int(regex_result.group()) + except: + return None + paths = folder_paths_get_folder_paths(model_path_type) + if model_path_index < 0 or model_path_index >= len(paths): + return None + model_path_span = regex_result.span() + return os.path.join( + comfyui_model_uri, + ( + paths[model_path_index] + + model_path[model_path_span[1]:] + ) + ) + + def get_safetensor_header(path): try: with open(path, "rb") as f: @@ -148,7 +172,7 @@ async def save_ui_settings(request): }) -@server.PromptServer.instance.routes.get("/model-manager/image-preview") +@server.PromptServer.instance.routes.get("/model-manager/image/preview") async def img_preview(request): uri = request.query.get("uri") @@ -179,7 +203,7 @@ async def img_preview(request): return web.Response(body=image_data, content_type="image/" + image_extension) -@server.PromptServer.instance.routes.get("/model-manager/models") +@server.PromptServer.instance.routes.get("/model-manager/models/list") async def load_download_models(request): model_types = os.listdir(comfyui_model_uri) model_types.remove("configs") @@ -221,11 +245,10 @@ async def load_download_models(request): model_items = [] for model, image, base_path_index, rel_path, date_modified, date_created in file_infos: - # TODO: Stop sending redundant path information item = { "name": model, - "searchPath": "/" + os.path.join(model_type, str(base_path_index), rel_path, model).replace(os.path.sep, "/"), # TODO: Remove hack - "path": os.path.join(rel_path, model), + "path": "/" + os.path.join(model_type, str(base_path_index), rel_path, model).replace(os.path.sep, "/"), # relative logical path + #"systemPath": os.path.join(rel_path, model), # relative system path (less information than "search path") "dateModified": date_modified, "dateCreated": date_created, #"dateLastUsed": "", # TODO: track server-side, send increment client-side @@ -293,7 +316,7 @@ def linear_directory_hierarchy(refresh = False): return dir_list -@server.PromptServer.instance.routes.get("/model-manager/model-directory-list") +@server.PromptServer.instance.routes.get("/model-manager/models/directory-list") async def directory_list(request): #body = await request.json() dir_list = linear_directory_hierarchy(True) @@ -387,7 +410,84 @@ def download_file(url, filename, overwrite): os.rename(filename_temp, filename) -@server.PromptServer.instance.routes.post("/model-manager/download") +@server.PromptServer.instance.routes.get("/model-manager/model/info") +async def get_model_info(request): + model_path = request.query.get("path", None) + if model_path is None: + return web.json_response({}) + model_path = urllib.parse.unquote(model_path) + + model_type = request.query.get("type") # TODO: in the searchPath? + if model_type is None: + return web.json_response({}) + model_type = urllib.parse.unquote(model_type) + + model_path_type = model_type_to_dir_name(model_type) + file = search_path_to_system_path(model_path, model_path_type) + if file is None: + return web.json_response({}) + + info = {} + path, name = os.path.split(model_path) + info["File Name"] = name + info["File Directory"] = path + info["File Size"] = os.path.getsize(file) + stats = pathlib.Path(file).stat() + date_format = "%Y/%m/%d %H:%M:%S" + info["Date Created"] = datetime.fromtimestamp(stats.st_ctime).strftime(date_format) + info["Date Modified"] = datetime.fromtimestamp(stats.st_mtime).strftime(date_format) + + header = get_safetensor_header(file) + metadata = header.get("__metadata__", None) + if metadata is not None: + info["Base Model"] = metadata.get("ss_sd_model_name", "") + info["Clip Skip"] = metadata.get("ss_clip_skip", "") + info["Hash"] = metadata.get("sshs_model_hash", "") + info["Output Name"] = metadata.get("ss_output_name", "") + + img_buckets = metadata.get("ss_bucket_info", "{}") + if type(img_buckets) is str: + img_buckets = json.loads(img_buckets) + resolutions = {} + if img_buckets is not None: + buckets = img_buckets.get("buckets", {}) + for resolution in buckets.values(): + dim = resolution["resolution"] + x, y = dim[0], dim[1] + count = resolution["count"] + resolutions[str(x) + "x" + str(y)] = count + resolutions = list(resolutions.items()) + resolutions.sort(key=lambda x: x[1], reverse=True) + info["Bucket Resolutions"] = resolutions + + dir_tags = metadata.get("ss_tag_frequency", "{}") + if type(dir_tags) is str: + dir_tags = json.loads(dir_tags) + tags = {} + for train_tags in dir_tags.values(): + for tag, count in train_tags.items(): + tags[tag] = tags.get(tag, 0) + count + tags = list(tags.items()) + tags.sort(key=lambda x: x[1], reverse=True) + info["Tags"] = tags + + file_name, _ = os.path.splitext(file) + txt_file = file_name + ".txt" + description = "" + if os.path.isfile(txt_file): + with open(txt_file, 'r', encoding="utf-8") as f: + description = f.read() + info["Description"] = description + + return web.json_response(info) + + +@server.PromptServer.instance.routes.get("/model-manager/system-separator") +async def get_system_separator(request): + return web.json_response(os.path.sep) + + +@server.PromptServer.instance.routes.post("/model-manager/model/download") async def download_model(request): body = await request.json() result = { @@ -403,24 +503,10 @@ async def download_model(request): result["invalid"] = "type" return web.json_response(result) model_path = body.get("path", "/0") - model_path = model_path.replace("/", os.path.sep) - regex_result = re.search(r'\d+', model_path) - if regex_result is None: - result["invalid"] = "type" - return web.json_response(result) - model_path_index = int(regex_result.group()) - paths = folder_paths_get_folder_paths(model_path_type) - if model_path_index < 0 or model_path_index >= len(paths): + directory = search_path_to_system_path(model_path, model_path_type) + if directory is None: result["invalid"] = "path" return web.json_response(result) - model_path_span = regex_result.span() - directory = os.path.join( - comfyui_model_uri, - ( - paths[model_path_index] + - model_path[model_path_span[1]:] - ) - ) download_uri = body.get("download") if download_uri is None: @@ -464,6 +550,52 @@ async def download_model(request): result["success"] = True return web.json_response(result) + +@server.PromptServer.instance.routes.post("/model-manager/model/delete") +async def delete_model(request): + result = { "success": False } + + model_path = request.query.get("path", None) + if model_path is None: + return web.json_response(result) + model_path = urllib.parse.unquote(model_path) + + model_type = request.query.get("type") # TODO: in the searchPath? + if model_type is None: + return web.json_response(result) + model_type = urllib.parse.unquote(model_type) + + model_path_type = model_type_to_dir_name(model_type) + file = search_path_to_system_path(model_path, model_path_type) + if file is None: + return web.json_response(result) + + is_model = None + for ext in folder_paths_get_supported_pt_extensions(model_type): + if file.endswith(ext): + is_model = True + break + if not is_model: + return web.json_response(result) + + if os.path.isfile(file): + os.remove(file) + result["success"] = True + + path_and_name, _ = os.path.splitext(file) + + for img_ext in image_extensions: + image_file = path_and_name + img_ext + if os.path.isfile(image_file): + os.remove(image_file) + + txt_file = path_and_name + ".txt" + if os.path.isfile(txt_file): + os.remove(txt_file) + + return web.json_response(result) + + WEB_DIRECTORY = "web" NODE_CLASS_MAPPINGS = {} __all__ = ["NODE_CLASS_MAPPINGS"] diff --git a/web/model-manager.css b/web/model-manager.css index 6ae11ac..d89613a 100644 --- a/web/model-manager.css +++ b/web/model-manager.css @@ -125,15 +125,23 @@ background-color: rgba(0, 0, 0, 0); } -.comfy-grid .model-preview-top-right { +.comfy-grid .model-preview-top-right, +.comfy-grid .model-preview-top-left { position: absolute; display: flex; flex-direction: column; gap: 8px; top: 8px; +} + +.comfy-grid .model-preview-top-right { right: 8px; } +.comfy-grid .model-preview-top-left { + left: 8px; +} + .comfy-grid .model-button { opacity: 0.65; } @@ -461,3 +469,28 @@ .model-manager [data-name="Download"] .download-settings { flex: 1; } + +.model-manager .model-info-view { + background-color: var(--comfy-menu-bg); + display: none; + height: 100%; + overflow-wrap: break-word; + width: 100%; +} + +.model-manager .model-info-container { + background-color: var(--bg-color); + border-radius: 16px; + color: var(--fg-color); + margin-top: 8px; + max-height: 90%; + padding: 16px; + overflow: auto; + width: auto; +} + +.model-manager .no-select { + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; +} diff --git a/web/model-manager.js b/web/model-manager.js index 3c7dc95..58bd38c 100644 --- a/web/model-manager.js +++ b/web/model-manager.js @@ -55,7 +55,7 @@ const MODEL_SORT_DATE_CREATED = "dateCreated"; const MODEL_SORT_DATE_MODIFIED = "dateModified"; const MODEL_SORT_DATE_NAME = "name"; -const MODEL_EXTENSIONS = [".ckpt", ".pt", ".bin", ".pth", ".safetensors"]; // TODO: ask server for? +const MODEL_EXTENSIONS = [".bin", ".ckpt", ".onnx", ".pt", ".pth", ".safetensors"]; // TODO: ask server for? const IMAGE_EXTENSIONS = [".apng", ".gif", ".jpeg", ".jpg", ".png", ".webp"]; // TODO: ask server for? /** @@ -174,10 +174,10 @@ function civitai_getModelFilesInfo(modelVersionInfo, type = null, fp = null, siz */ async function civitai_getFilteredInfo(stringUrl) { const url = new URL(stringUrl); - if (url.hostname != 'civitai.com') { return {}; } - if (url.pathname == '/') { return {} } + if (url.hostname != "civitai.com") { return {}; } + if (url.pathname == "/") { return {} } const urlPath = url.pathname; - if (urlPath.startsWith('/api')) { + if (urlPath.startsWith("/api")) { const idEnd = urlPath.length - (urlPath.at(-1) == "/" ? 1 : 0); const idStart = urlPath.lastIndexOf("/", idEnd - 1) + 1; const modelVersionId = urlPath.substring(idStart, idEnd); @@ -260,8 +260,8 @@ async function huggingFace_requestInfo(id, apiPath = "models") { */ async function huggingFace_getFilteredInfo(stringUrl) { const url = new URL(stringUrl); - if (url.hostname != 'huggingface.co') { return {}; } - if (url.pathname == '/') { return {} } + if (url.hostname != "huggingface.co") { return {}; } + if (url.pathname == "/") { return {} } const urlPath = url.pathname; const i0 = 1; const i1 = urlPath.indexOf("/", i0); @@ -376,10 +376,10 @@ class DirectoryDropdown { * @param {Function} updateDropdown * @param {Function} [updateCallback= () => {}] * @param {Function} [submitCallback= () => {}] - * @param {String} [sep="/"] + * @param {String} [searchSeparator="/"] * @param {Boolean} [showDirectoriesOnly=false] */ - constructor(input, updateDropdown, updateCallback = () => {}, submitCallback = () => {}, sep = "/", showDirectoriesOnly = false) { + constructor(input, updateDropdown, updateCallback = () => {}, submitCallback = () => {}, searchSeparator = "/", showDirectoriesOnly = false) { /** @type {HTMLDivElement} */ const dropdown = $el("div.search-dropdown", { // TODO: change to `search-directory-dropdown` style: { @@ -423,7 +423,7 @@ class DirectoryDropdown { e.stopPropagation(); e.preventDefault(); // prevent cursor move const input = e.target; - DirectoryDropdown.selectionToInput(input, selection, sep); + DirectoryDropdown.selectionToInput(input, selection, searchSeparator); updateDropdown(); //updateCallback(); //submitCallback(); @@ -439,11 +439,11 @@ class DirectoryDropdown { else if (e.key === "ArrowLeft" && dropdown.style.display !== "none") { const input = e.target; const oldFilterText = input.value; - const iSep = oldFilterText.lastIndexOf(sep, oldFilterText.length - 2); + const iSep = oldFilterText.lastIndexOf(searchSeparator, oldFilterText.length - 2); const newFilterText = oldFilterText.substring(0, iSep + 1); if (oldFilterText !== newFilterText) { const delta = oldFilterText.substring(iSep + 1); - let isMatch = delta[delta.length-1] === sep; + let isMatch = delta[delta.length-1] === searchSeparator; if (!isMatch) { const options = dropdown.children; for (let i = 0; i < options.length; i++) { @@ -488,7 +488,7 @@ class DirectoryDropdown { const input = e.target const selection = options[iSelection]; if (selection !== undefined && selection !== null) { - DirectoryDropdown.selectionToInput(input, selection, sep); + DirectoryDropdown.selectionToInput(input, selection, searchSeparator); updateDropdown(); updateCallback(); } @@ -542,23 +542,23 @@ class DirectoryDropdown { /** * @param {HTMLInputElement} input * @param {HTMLParagraphElement | undefined | null} selection - * @param {String} [sep="/"] + * @param {String} searchSeparator */ - static selectionToInput(input, selection, sep) { + static selectionToInput(input, selection, searchSeparator) { selection.classList.remove(DROPDOWN_DIRECTORY_SELECTION_CLASS); const selectedText = selection.innerText; const oldFilterText = input.value; - const iSep = oldFilterText.lastIndexOf(sep); + const iSep = oldFilterText.lastIndexOf(searchSeparator); const previousPath = oldFilterText.substring(0, iSep + 1); input.value = previousPath + selectedText; } /** * @param {DirectoryItem[]} directories - * @param {string} sep + * @param {string} searchSeparator * @param {string} [modelType = ""] */ - update(directories, sep, modelType = "") { + update(directories, searchSeparator, modelType = "") { const dropdown = this.element; const input = this.#input; const updateDropdown = this.#updateDropdown; @@ -567,7 +567,7 @@ class DirectoryDropdown { const showDirectoriesOnly = this.showDirectoriesOnly; const filter = input.value; - if (filter[0] !== sep) { + if (filter[0] !== searchSeparator) { dropdown.style.display = "none"; return; } @@ -590,7 +590,7 @@ class DirectoryDropdown { // TODO: directories === undefined? let indexLastWord = 1; while (true) { - const indexNextWord = filter.indexOf(sep, indexLastWord); + const indexNextWord = filter.indexOf(searchSeparator, indexLastWord); if (indexNextWord === -1) { // end of filter break; @@ -643,7 +643,7 @@ class DirectoryDropdown { const isDir = grandChildCount !== undefined && grandChildCount !== null && grandChildCount > 0; const itemName = child["name"]; if (itemName.startsWith(lastWord) && (!showDirectoriesOnly || (showDirectoriesOnly && isDir))) { - options.push(itemName + (isDir ? "/" : "")); + options.push(itemName + (isDir ? searchSeparator : "")); } } } @@ -680,7 +680,7 @@ class DirectoryDropdown { const selection_submit = (e) => { e.stopPropagation(); const selection = e.target; - DirectoryDropdown.selectionToInput(input, selection, sep); + DirectoryDropdown.selectionToInput(input, selection, searchSeparator); updateDropdown(); updateCallback();e.target submitCallback(); @@ -729,6 +729,16 @@ function pathToFileString(path) { return path.slice(i); } +/** + * @param {string} path + * @returns {string} + */ +function searchPathToSystemPath(path, searchSeparator, systemSeparator) { + const i1 = path.indexOf(searchSeparator, 1); + const i2 = path.indexOf(searchSeparator, i1 + 1); + return path.slice(i2 + 1).replaceAll(searchSeparator, systemSeparator); +} + /** * @param {string} file * @returns {string | undefined} @@ -764,6 +774,9 @@ function insertEmbeddingIntoText(text, file, removeExtension) { * @param {string} [resetText=""] */ function buttonAlert(element, success, successText = "", failureText = "", resetText = "") { + if (element === undefined || element === null) { + return; + } const name = success ? "button-success" : "button-failure"; element.classList.add(name); if (successText != "" && failureText != "") { @@ -775,7 +788,7 @@ function buttonAlert(element, success, successText = "", failureText = "", reset if (innerHTML != "") { element.innerHTML = innerHTML; } - }, 500, element, name, resetText); + }, 1000, element, name, resetText); } class Tabs { @@ -874,7 +887,7 @@ class ModelGrid { .filter(Boolean); const regexSHA256 = /^[a-f0-9]{64}$/gi; - const fields = ["name", "searchPath"]; // TODO: Remove "searchPath" hack. + const fields = ["name", "path"]; return list.filter((element) => { const text = fields .reduce((memo, field) => memo + " " + element[field], "") @@ -1063,9 +1076,12 @@ class ModelGrid { * @param {Array} models * @param {string} modelType * @param {Object.} settingsElements + * @param {String} searchSeparator + * @param {String} systemSeparator + * @param {Function} modelInfoCallback * @returns {HTMLElement[]} */ - static #generateInnerHtml(models, modelType, settingsElements) { + static #generateInnerHtml(models, modelType, settingsElements, searchSeparator, systemSeparator, modelInfoCallback) { // TODO: seperate text and model logic; getting too messy // TODO: fallback on button failure to copy text? const canShowButtons = modelNodeType[modelType] !== undefined; @@ -1078,14 +1094,21 @@ class ModelGrid { if (models.length > 0) { return models.map((item) => { const uri = item.post ?? "no-post"; - const imgUrl = `/model-manager/image-preview?uri=${uri}`; + const imgUrl = `/model-manager/image/preview?uri=${uri}`; + const searchPath = item.path; + const path = searchPathToSystemPath(searchPath, searchSeparator, systemSeparator); let buttons = []; if (showAddButton) { buttons.push( $el("button.icon-button.model-button", { type: "button", textContent: "⧉︎", - onclick: (e) => ModelGrid.#copyModelToClipboard(e, modelType, item.path, removeEmbeddingExtension), + onclick: (e) => ModelGrid.#copyModelToClipboard( + e, + modelType, + path, + removeEmbeddingExtension + ), draggable: false, }) ); @@ -1095,12 +1118,24 @@ class ModelGrid { $el("button.icon-button.model-button", { type: "button", textContent: "✚", - onclick: (e) => ModelGrid.#addModel(e, modelType, item.path, removeEmbeddingExtension, addOffset), + onclick: (e) => ModelGrid.#addModel( + e, + modelType, + path, + removeEmbeddingExtension, + addOffset + ), draggable: false, }) ); } - const dragAdd = (e) => ModelGrid.#dragAddModel(e, modelType, item.path, removeEmbeddingExtension, strictDragToAdd); + const dragAdd = (e) => ModelGrid.#dragAddModel( + e, + modelType, + path, + removeEmbeddingExtension, + strictDragToAdd + ); return $el("div.item", {}, [ $el("img.model-preview", { src: imgUrl, @@ -1115,6 +1150,16 @@ class ModelGrid { }, buttons ), + $el("div.model-preview-top-left", { + draggable: false, + }, [ + $el("button.icon-button.model-button", { + type: "button", + textContent: "ⓘ", + onclick: async() => modelInfoCallback(modelType, searchPath), + draggable: false, + }), + ]), $el("div.model-label", { ondragend: (e) => dragAdd(e), draggable: true, @@ -1138,8 +1183,11 @@ class ModelGrid { * @param {boolean} reverseSort * @param {Array} previousModelFilters * @param {HTMLInputElement} modelFilter + * @param {String} searchSeparator + * @param {String} systemSeparator + * @param {Function} modelInfoCallback */ - static update(modelGrid, models, modelSelect, previousModelType, settings, sortBy, reverseSort, previousModelFilters, modelFilter) { + static update(modelGrid, models, modelSelect, previousModelType, settings, sortBy, reverseSort, previousModelFilters, modelFilter, searchSeparator, systemSeparator, modelInfoCallback) { let modelType = modelSelect.value; if (models[modelType] === undefined) { modelType = "checkpoints"; // TODO: magic value @@ -1173,7 +1221,14 @@ class ModelGrid { ModelGrid.#sort(modelList, sortBy, reverseSort); modelGrid.innerHTML = ""; - const modelGridModels = ModelGrid.#generateInnerHtml(modelList, modelType, settings); + const modelGridModels = ModelGrid.#generateInnerHtml( + modelList, + modelType, + settings, + searchSeparator, + systemSeparator, + modelInfoCallback, + ); modelGrid.append.apply(modelGrid, modelGridModels); } } @@ -1221,6 +1276,8 @@ function $radioGroup(attr) { class ModelManager extends ComfyDialog { #el = { + /** @type {HTMLDivElement} */ modelInfoView: null, + /** @type {HTMLDivElement} */ modelInfoContainer: null, /** @type {HTMLDivElement} */ modelInfoUrl: null, /** @type {HTMLDivElement} */ modelInfos: null, @@ -1257,7 +1314,10 @@ class ModelManager extends ComfyDialog { }; /** @type {string} */ - #sep = "/"; + #searchSeparator = "/"; + + /** @type {string} */ + #systemSeparator = null; constructor() { super(); @@ -1268,6 +1328,53 @@ class ModelManager extends ComfyDialog { }, [ $el("div.comfy-modal-content", [ // TODO: settings.top_bar_left_to_right or settings.top_bar_right_to_left + $el("div.model-info-view", { + $: (el) => (this.#el.modelInfoView = el), + }, [ + $el("div", { + style: { + display: "flex", + gap: "8px", + }, + }, [ + $el("button.icon-button", { + textContent: "🗑︎", + onclick: async(e) => { + const affirmation = "delete"; + const confirmation = window.prompt("Type \"" + affirmation + "\" to delete the model PERMANENTLY?\n\nThis includes all image or text files."); + let deleted = false; + if (confirmation === affirmation) { + const container = this.#el.modelInfoContainer; + const path = encodeURIComponent(container.dataset.path); + const type = encodeURIComponent(this.#el.modelTypeSelect.value); + await request( + `/model-manager/model/delete?path=${path}&type=${type}`, + { + method: "POST", + } + ) + .then((result) => { + if (result["success"]) + { + container.innerHTML = ""; + this.#el.modelInfoView.style.display = "none"; + this.#modelTab_updateModels(); + deleted = true; + } + }) + .catch(err => {}); + } + if (!deleted) { + buttonAlert(e.target, false); + } + }, + }), + ]), + $el("div.model-info-container", { + $: (el) => (this.#el.modelInfoContainer = el), + "data-path": "", + }), + ]), $el("div.topbar-buttons", [ $el("div.sidebar-buttons", @@ -1294,7 +1401,15 @@ class ModelManager extends ComfyDialog { ]), $el("button.icon-button", { textContent: "✖", - onclick: () => this.close(), + onclick: () => { + const infoView = this.#el.modelInfoView; + if (infoView.style.display === "none") { + this.close(); + } + else { + infoView.style.display = "none"; + } + }, }), ] ), @@ -1336,7 +1451,7 @@ class ModelManager extends ComfyDialog { this.#modelTab_updateDirectoryDropdown, this.#modelTab_updatePreviousModelFilter, this.#modelTab_updateModelGrid, - this.#sep, + this.#searchSeparator, false, ); this.#modelContentFilterDirectoryDropdown = searchDropdown; @@ -1398,13 +1513,17 @@ class ModelManager extends ComfyDialog { sortBy, reverseSort, this.#data.previousModelFilters, - this.#el.modelContentFilter + this.#el.modelContentFilter, + this.#searchSeparator, + this.#systemSeparator, + this.#modelTab_showModelInfo, ); } async #modelTab_updateModels() { - this.#data.models = await request("/model-manager/models"); - const newModelDirectories = await request("/model-manager/model-directory-list"); + this.#systemSeparator = await request("/model-manager/system-separator"); + this.#data.models = await request("/model-manager/models/list"); + const newModelDirectories = await request("/model-manager/models/directory-list"); this.#data.modelDirectories.splice(0, Infinity, ...newModelDirectories); // note: do NOT create a new array this.#modelTab_updateModelGrid(); } @@ -1418,12 +1537,69 @@ class ModelManager extends ComfyDialog { #modelTab_updateDirectoryDropdown = () => { this.#modelContentFilterDirectoryDropdown.update( this.#data.modelDirectories, - this.#sep, + this.#searchSeparator, this.#el.modelTypeSelect.value, ); this.#modelTab_updatePreviousModelFilter(); } + /** + * @param {string} modelType + * @param {string} searchPath + */ + #modelTab_showModelInfo = async(modelType, searchPath) => { + const type = encodeURIComponent(modelType); + const path = encodeURIComponent(searchPath); + const info = await request(`/model-manager/model/info?path=${path}&type=${type}`) + .catch(err => { + console.log(err); + return null; + }); + if (info === null) { + return; + } + const infoHtml = this.#el.modelInfoContainer; + infoHtml.innerHTML = ""; + infoHtml.dataset.path = searchPath; + const innerHtml = []; + const filename = info["File Name"]; + if (filename !== undefined && filename !== null && filename !== "") { + innerHtml.push($el("h1", [filename])); + } + for (const [key, value] of Object.entries(info)) { + if (value === undefined || value === null || value === "") { + continue; + } + + if (Array.isArray(value)) { + if (value.length > 0) { + innerHtml.push($el("h2", [key + ":"])); + + let text = "

"; + for (let i = 0; i < value.length; i++) { + const v = value[i]; + const tag = v[0]; + const count = v[1]; + text += tag + " (" + count + ")"; + if (i !== value.length - 1) { + text += ", "; + } + } + text += "

"; + const div = $el("div"); + div.innerHTML = text; + innerHtml.push(div); + } + } + else { + innerHtml.push($el("p", [key + ": " + value])); + } + } + infoHtml.append.apply(infoHtml, innerHtml); + + this.#el.modelInfoView.style.display = "block"; + } + /** * @param {HTMLInputElement[]} settings * @param {boolean} reloadData @@ -1482,7 +1658,9 @@ class ModelManager extends ComfyDialog { method: "POST", body: JSON.stringify({ "settings": settings }), } - ); + ).catch((err) => { + return { "success": false }; + }); const success = data["success"]; if (success) { const settings = data["settings"]; @@ -1636,7 +1814,7 @@ class ModelManager extends ComfyDialog { modelManager.classList.add(newSidebarState); } } - + /** * @param {HTMLDivElement} previewImageContainer * @param {Event} e @@ -1661,16 +1839,16 @@ class ModelManager extends ComfyDialog { else if (currentIndex < 0) { currentIndex = children.length - 1; } children[currentIndex].style.display = "block"; } - + /** * @param {Object} info * @param {String[]} modelTypes * @param {DirectoryItem[]} modelDirectories - * @param {String} sep + * @param {String} searchSeparator * @param {int} id * @returns {HTMLDivElement} */ - #downloadTab_modelInfo(info, modelTypes, modelDirectories, sep, id) { + #downloadTab_modelInfo(info, modelTypes, modelDirectories, searchSeparator, id) { // TODO: use passed in info const RADIO_MODEL_PREVIEW_NONE = "No Preview"; const RADIO_MODEL_PREVIEW_DEFAULT = "Default Preview"; @@ -1715,13 +1893,13 @@ class ModelManager extends ComfyDialog { if (modelType === "") { return; } searchDropdown.update( modelDirectories, - sep, + searchSeparator, modelType, ); }, () => {}, () => {}, - sep, + searchSeparator, true, ); @@ -1869,7 +2047,7 @@ class ModelManager extends ComfyDialog { let success = true; let resultText = "✔"; await request( - "/model-manager/download", + "/model-manager/model/download", { method: "POST", body: JSON.stringify(record), @@ -1951,7 +2129,7 @@ class ModelManager extends ComfyDialog { return modelInfo; } - + async #downloadTab_search() { const infosHtml = this.#el.modelInfos; infosHtml.innerHTML = ""; @@ -2039,7 +2217,7 @@ class ModelManager extends ComfyDialog { modelInfo, modelTypes, this.#data.modelDirectories, - this.#sep, + this.#searchSeparator, id, ); });