Added "View Model Info" and "Delete Model" functionality.

NOTE: Some small (but important) changes under the hood that may have broken things. "Quality control" tests were done, but may be have missed things.

- View model info in Model Tab.
- Delete model in Model Info view. (Uses Co
- Reduced model path (search path) in models request and added system separator request.
- Reworked "system path" "search path", "system separator" and "search separator" (a bit messy, maybe needs another rework).
- Updated REST uri structure to be more consistent.
This commit is contained in:
Christian Bastian
2024-02-17 13:30:58 -05:00
parent 8fbbff2175
commit acc15298bc
3 changed files with 414 additions and 71 deletions

View File

@@ -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"]

View File

@@ -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;
}

View File

@@ -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.<HTMLInputElement>} 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 = "<p>";
for (let i = 0; i < value.length; i++) {
const v = value[i];
const tag = v[0];
const count = v[1];
text += tag + "<span class=\"no-select\"> (" + count + ")</span>";
if (i !== value.length - 1) {
text += ", ";
}
}
text += "</p>";
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,
);
});