diff --git a/README.md b/README.md
index 90931141d..877f46433 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
ComfyUI
=======
-A powerful and modular stable diffusion GUI.
+A powerful and modular stable diffusion GUI and backend.
-----------

@@ -146,9 +146,9 @@ This will let you use: pip3.10 to install all the dependencies.
## How to increase generation speed?
-Make sure you use the CheckpointLoaderSimple node to load checkpoints. It will auto pick the right settings depending on your GPU.
+Make sure you use the regular loaders/Load Checkpoint node to load checkpoints. It will auto pick the right settings depending on your GPU.
-You can set this command line setting to disable the upcasting to fp32 in some cross attention operations which will increase your speed. Note that this doesn't do anything when xformers is enabled and will very likely give you black images on SD2.x models.
+You can set this command line setting to disable the upcasting to fp32 in some cross attention operations which will increase your speed. Note that this will very likely give you black images on SD2.x models. If you use xformers this option does not do anything.
```--dont-upcast-attention```
diff --git a/comfy/cli_args.py b/comfy/cli_args.py
index 739891f71..b24054ce0 100644
--- a/comfy/cli_args.py
+++ b/comfy/cli_args.py
@@ -26,6 +26,6 @@ vram_group.add_argument("--cpu", action="store_true", help="To use the CPU for e
parser.add_argument("--dont-print-server", action="store_true", help="Don't print server output.")
parser.add_argument("--quick-test-for-ci", action="store_true", help="Quick test for CI.")
-parser.add_argument("--windows-standalone-build", action="store_true", help="Windows standalone build.")
+parser.add_argument("--windows-standalone-build", action="store_true", help="Windows standalone build: Enable convenient things that most people using the standalone windows build will probably enjoy (like auto opening the page on startup).")
args = parser.parse_args()
diff --git a/comfy/clip_vision.py b/comfy/clip_vision.py
index cb29df432..efb2d5384 100644
--- a/comfy/clip_vision.py
+++ b/comfy/clip_vision.py
@@ -1,6 +1,7 @@
from transformers import CLIPVisionModelWithProjection, CLIPVisionConfig, CLIPImageProcessor
from .utils import load_torch_file, transformers_convert
import os
+import torch
class ClipVisionModel():
def __init__(self, json_config):
@@ -20,7 +21,8 @@ class ClipVisionModel():
self.model.load_state_dict(sd, strict=False)
def encode_image(self, image):
- inputs = self.processor(images=[image[0]], return_tensors="pt")
+ img = torch.clip((255. * image[0]), 0, 255).round().int()
+ inputs = self.processor(images=[img], return_tensors="pt")
outputs = self.model(**inputs)
return outputs
diff --git a/comfy/model_management.py b/comfy/model_management.py
index 2407140fd..8303cb437 100644
--- a/comfy/model_management.py
+++ b/comfy/model_management.py
@@ -45,6 +45,8 @@ try:
except:
OOM_EXCEPTION = Exception
+XFORMERS_VERSION = ""
+XFORMERS_ENABLED_VAE = True
if args.disable_xformers:
XFORMERS_IS_AVAILABLE = False
else:
@@ -52,6 +54,17 @@ else:
import xformers
import xformers.ops
XFORMERS_IS_AVAILABLE = True
+ try:
+ XFORMERS_VERSION = xformers.version.__version__
+ print("xformers version:", XFORMERS_VERSION)
+ if XFORMERS_VERSION.startswith("0.0.18"):
+ print()
+ print("WARNING: This version of xformers has a major bug where you will get black images when generating high resolution images.")
+ print("Please downgrade or upgrade xformers to a different version.")
+ print()
+ XFORMERS_ENABLED_VAE = False
+ except:
+ pass
except:
XFORMERS_IS_AVAILABLE = False
@@ -223,13 +236,8 @@ def xformers_enabled_vae():
enabled = xformers_enabled()
if not enabled:
return False
- try:
- #0.0.18 has a bug where Nan is returned when inputs are too big (1152x1920 res images and above)
- if xformers.version.__version__ == "0.0.18":
- return False
- except:
- pass
- return enabled
+
+ return XFORMERS_ENABLED_VAE
def pytorch_attention_enabled():
return ENABLE_PYTORCH_ATTENTION
diff --git a/comfyui_screenshot.png b/comfyui_screenshot.png
index c357e2439..73272eae6 100644
Binary files a/comfyui_screenshot.png and b/comfyui_screenshot.png differ
diff --git a/custom_nodes/example_node.py.example b/custom_nodes/example_node.py.example
index fb8172648..175202aeb 100644
--- a/custom_nodes/example_node.py.example
+++ b/custom_nodes/example_node.py.example
@@ -88,3 +88,8 @@ class Example:
NODE_CLASS_MAPPINGS = {
"Example": Example
}
+
+# A dictionary that contains the friendly/humanly readable titles for the nodes
+NODE_DISPLAY_NAME_MAPPINGS = {
+ "Example": "Example Node"
+}
diff --git a/nodes.py b/nodes.py
index 9a5e72ea4..380246e1c 100644
--- a/nodes.py
+++ b/nodes.py
@@ -1106,6 +1106,54 @@ NODE_CLASS_MAPPINGS = {
"DiffusersLoader": DiffusersLoader,
}
+NODE_DISPLAY_NAME_MAPPINGS = {
+ # Sampling
+ "KSampler": "KSampler",
+ "KSamplerAdvanced": "KSampler (Advanced)",
+ # Loaders
+ "CheckpointLoader": "Load Checkpoint (With Config)",
+ "CheckpointLoaderSimple": "Load Checkpoint",
+ "VAELoader": "Load VAE",
+ "LoraLoader": "Load LoRA",
+ "CLIPLoader": "Load CLIP",
+ "ControlNetLoader": "Load ControlNet Model",
+ "DiffControlNetLoader": "Load ControlNet Model (diff)",
+ "StyleModelLoader": "Load Style Model",
+ "CLIPVisionLoader": "Load CLIP Vision",
+ "UpscaleModelLoader": "Load Upscale Model",
+ # Conditioning
+ "CLIPVisionEncode": "CLIP Vision Encode",
+ "StyleModelApply": "Apply Style Model",
+ "CLIPTextEncode": "CLIP Text Encode (Prompt)",
+ "CLIPSetLastLayer": "CLIP Set Last Layer",
+ "ConditioningCombine": "Conditioning (Combine)",
+ "ConditioningSetArea": "Conditioning (Set Area)",
+ "ControlNetApply": "Apply ControlNet",
+ # Latent
+ "VAEEncodeForInpaint": "VAE Encode (for Inpainting)",
+ "SetLatentNoiseMask": "Set Latent Noise Mask",
+ "VAEDecode": "VAE Decode",
+ "VAEEncode": "VAE Encode",
+ "LatentRotate": "Rotate Latent",
+ "LatentFlip": "Flip Latent",
+ "LatentCrop": "Crop Latent",
+ "EmptyLatentImage": "Empty Latent Image",
+ "LatentUpscale": "Upscale Latent",
+ "LatentComposite": "Latent Composite",
+ # Image
+ "SaveImage": "Save Image",
+ "PreviewImage": "Preview Image",
+ "LoadImage": "Load Image",
+ "LoadImageMask": "Load Image (as Mask)",
+ "ImageScale": "Upscale Image",
+ "ImageUpscaleWithModel": "Upscale Image (using Model)",
+ "ImageInvert": "Invert Image",
+ "ImagePadForOutpaint": "Pad Image for Outpainting",
+ # _for_testing
+ "VAEDecodeTiled": "VAE Decode (Tiled)",
+ "VAEEncodeTiled": "VAE Encode (Tiled)",
+}
+
def load_custom_node(module_path):
module_name = os.path.basename(module_path)
if os.path.isfile(module_path):
@@ -1121,6 +1169,8 @@ def load_custom_node(module_path):
module_spec.loader.exec_module(module)
if hasattr(module, "NODE_CLASS_MAPPINGS") and getattr(module, "NODE_CLASS_MAPPINGS") is not None:
NODE_CLASS_MAPPINGS.update(module.NODE_CLASS_MAPPINGS)
+ if hasattr(module, "NODE_DISPLAY_NAME_MAPPINGS") and getattr(module, "NODE_DISPLAY_NAME_MAPPINGS") is not None:
+ NODE_DISPLAY_NAME_MAPPINGS.update(module.NODE_DISPLAY_NAME_MAPPINGS)
else:
print(f"Skip {module_path} module for custom nodes due to the lack of NODE_CLASS_MAPPINGS.")
except Exception as e:
diff --git a/notebooks/comfyui_colab.ipynb b/notebooks/comfyui_colab.ipynb
index 3e59fbde7..071a89969 100644
--- a/notebooks/comfyui_colab.ipynb
+++ b/notebooks/comfyui_colab.ipynb
@@ -47,7 +47,7 @@
" !git pull\n",
"\n",
"!echo -= Install dependencies =-\n",
- "!pip install xformers -r requirements.txt --extra-index-url https://download.pytorch.org/whl/cu118"
+ "!pip install xformers!=0.0.18 -r requirements.txt --extra-index-url https://download.pytorch.org/whl/cu118"
]
},
{
@@ -86,6 +86,11 @@
"#!wget -c https://huggingface.co/waifu-diffusion/wd-1-5-beta2/resolve/main/checkpoints/wd-1-5-beta2-fp16.safetensors -P ./models/checkpoints/\n",
"\n",
"\n",
+ "# unCLIP models\n",
+ "#!wget -c https://huggingface.co/comfyanonymous/illuminatiDiffusionV1_v11_unCLIP/resolve/main/illuminatiDiffusionV1_v11-unclip-h-fp16.safetensors -P ./models/checkpoints/\n",
+ "#!wget -c https://huggingface.co/comfyanonymous/wd-1.5-beta2_unCLIP/resolve/main/wd-1-5-beta2-aesthetic-unclip-h-fp16.safetensors -P ./models/checkpoints/\n",
+ "\n",
+ "\n",
"# VAE\n",
"!wget -c https://huggingface.co/stabilityai/sd-vae-ft-mse-original/resolve/main/vae-ft-mse-840000-ema-pruned.safetensors -P ./models/vae/\n",
"#!wget -c https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/VAEs/orangemix.vae.pt -P ./models/vae/\n",
diff --git a/server.py b/server.py
index 81686afee..51d43bcc4 100644
--- a/server.py
+++ b/server.py
@@ -214,7 +214,8 @@ class PromptServer():
info['input'] = obj_class.INPUT_TYPES()
info['output'] = obj_class.RETURN_TYPES
info['output_name'] = obj_class.RETURN_NAMES if hasattr(obj_class, 'RETURN_NAMES') else info['output']
- info['name'] = x #TODO
+ info['name'] = x
+ info['display_name'] = nodes.NODE_DISPLAY_NAME_MAPPINGS[x] if x in nodes.NODE_DISPLAY_NAME_MAPPINGS.keys() else x
info['description'] = ''
info['category'] = 'sd'
if hasattr(obj_class, 'CATEGORY'):
diff --git a/web/extensions/core/colorPalette.js b/web/extensions/core/colorPalette.js
index e54bc2a38..a08d46684 100644
--- a/web/extensions/core/colorPalette.js
+++ b/web/extensions/core/colorPalette.js
@@ -21,28 +21,74 @@ const colorPalettes = {
"MODEL": "#B39DDB", // light lavender-purple
"STYLE_MODEL": "#C2FFAE", // light green-yellow
"VAE": "#FF6E6E", // bright red
- }
- }
+ },
+ "litegraph_base": {
+ "NODE_TITLE_COLOR": "#999",
+ "NODE_SELECTED_TITLE_COLOR": "#FFF",
+ "NODE_TEXT_SIZE": 14,
+ "NODE_TEXT_COLOR": "#AAA",
+ "NODE_SUBTEXT_SIZE": 12,
+ "NODE_DEFAULT_COLOR": "#333",
+ "NODE_DEFAULT_BGCOLOR": "#353535",
+ "NODE_DEFAULT_BOXCOLOR": "#666",
+ "NODE_DEFAULT_SHAPE": "box",
+ "NODE_BOX_OUTLINE_COLOR": "#FFF",
+ "DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.5)",
+ "DEFAULT_GROUP_FONT": 24,
+
+ "WIDGET_BGCOLOR": "#222",
+ "WIDGET_OUTLINE_COLOR": "#666",
+ "WIDGET_TEXT_COLOR": "#DDD",
+ "WIDGET_SECONDARY_TEXT_COLOR": "#999",
+
+ "LINK_COLOR": "#9A9",
+ "EVENT_LINK_COLOR": "#A86",
+ "CONNECTING_LINK_COLOR": "#AFA",
+ },
+ },
},
- "palette_2": {
- "id": "palette_2",
- "name": "Palette 2",
+ "solarized": {
+ "id": "solarized",
+ "name": "Solarized",
"colors": {
"node_slot": {
- "CLIP": "#556B2F", // Dark Olive Green
- "CLIP_VISION": "#4B0082", // Indigo
- "CLIP_VISION_OUTPUT": "#006400", // Green
- "CONDITIONING": "#FF1493", // Deep Pink
- "CONTROL_NET": "#8B4513", // Saddle Brown
- "IMAGE": "#8B0000", // Dark Red
- "LATENT": "#00008B", // Dark Blue
- "MASK": "#2F4F4F", // Dark Slate Grey
- "MODEL": "#FF8C00", // Dark Orange
- "STYLE_MODEL": "#004A4A", // Sherpa Blue
- "UPSCALE_MODEL": "#4A004A", // Tyrian Purple
- "VAE": "#4F394F", // Loulou
- }
- }
+ "CLIP": "#859900", // Green
+ "CLIP_VISION": "#6c71c4", // Indigo
+ "CLIP_VISION_OUTPUT": "#859900", // Green
+ "CONDITIONING": "#d33682", // Magenta
+ "CONTROL_NET": "#cb4b16", // Orange
+ "IMAGE": "#dc322f", // Red
+ "LATENT": "#268bd2", // Blue
+ "MASK": "#073642", // Base02
+ "MODEL": "#cb4b16", // Orange
+ "STYLE_MODEL": "#073642", // Base02
+ "UPSCALE_MODEL": "#6c71c4", // Indigo
+ "VAE": "#586e75", // Base1
+ },
+ "litegraph_base": {
+ "NODE_TITLE_COLOR": "#fdf6e3",
+ "NODE_SELECTED_TITLE_COLOR": "#b58900",
+ "NODE_TEXT_SIZE": 14,
+ "NODE_TEXT_COLOR": "#657b83",
+ "NODE_SUBTEXT_SIZE": 12,
+ "NODE_DEFAULT_COLOR": "#586e75",
+ "NODE_DEFAULT_BGCOLOR": "#073642",
+ "NODE_DEFAULT_BOXCOLOR": "#839496",
+ "NODE_DEFAULT_SHAPE": "box",
+ "NODE_BOX_OUTLINE_COLOR": "#fdf6e3",
+ "DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.5)",
+ "DEFAULT_GROUP_FONT": 24,
+
+ "WIDGET_BGCOLOR": "#002b36",
+ "WIDGET_OUTLINE_COLOR": "#839496",
+ "WIDGET_TEXT_COLOR": "#fdf6e3",
+ "WIDGET_SECONDARY_TEXT_COLOR": "#93a1a1",
+
+ "LINK_COLOR": "#2aa198",
+ "EVENT_LINK_COLOR": "#268bd2",
+ "CONNECTING_LINK_COLOR": "#859900",
+ },
+ },
}
};
@@ -192,8 +238,20 @@ app.registerExtension({
if (colorPalette.colors) {
if (colorPalette.colors.node_slot) {
Object.assign(app.canvas.default_connection_color_byType, colorPalette.colors.node_slot);
- app.canvas.draw(true, true);
+ Object.assign(LGraphCanvas.link_type_colors, colorPalette.colors.node_slot);
}
+ if (colorPalette.colors.litegraph_base) {
+ // Everything updates correctly in the loop, except the Node Title and Link Color for some reason
+ app.canvas.node_title_color = colorPalette.colors.litegraph_base.NODE_TITLE_COLOR;
+ app.canvas.default_link_color = colorPalette.colors.litegraph_base.LINK_COLOR;
+
+ for (const key in colorPalette.colors.litegraph_base) {
+ if (colorPalette.colors.litegraph_base.hasOwnProperty(key) && LiteGraph.hasOwnProperty(key)) {
+ LiteGraph[key] = colorPalette.colors.litegraph_base[key];
+ }
+ }
+ }
+ app.canvas.draw(true, true);
}
};
diff --git a/web/extensions/core/nodeTemplates.js b/web/extensions/core/nodeTemplates.js
new file mode 100644
index 000000000..69d09cde8
--- /dev/null
+++ b/web/extensions/core/nodeTemplates.js
@@ -0,0 +1,184 @@
+import { app } from "/scripts/app.js";
+import { ComfyDialog, $el } from "/scripts/ui.js";
+
+// Adds the ability to save and add multiple nodes as a template
+// To save:
+// Select multiple nodes (ctrl + drag to select a region or ctrl+click individual nodes)
+// Right click the canvas
+// Save Node Template -> give it a name
+//
+// To add:
+// Right click the canvas
+// Node templates -> click the one to add
+//
+// To delete/rename:
+// Right click the canvas
+// Node templates -> Manage
+
+const id = "Comfy.NodeTemplates";
+
+class ManageTemplates extends ComfyDialog {
+ constructor() {
+ super();
+ this.element.classList.add("comfy-manage-templates");
+ this.templates = this.load();
+ }
+
+ createButtons() {
+ const btns = super.createButtons();
+ btns[0].textContent = "Cancel";
+ btns.unshift(
+ $el("button", {
+ type: "button",
+ textContent: "Save",
+ onclick: () => this.save(),
+ })
+ );
+ return btns;
+ }
+
+ load() {
+ const templates = localStorage.getItem(id);
+ if (templates) {
+ return JSON.parse(templates);
+ } else {
+ return [];
+ }
+ }
+
+ save() {
+ // Find all visible inputs and save them as our new list
+ const inputs = this.element.querySelectorAll("input");
+ const updated = [];
+
+ for (let i = 0; i < inputs.length; i++) {
+ const input = inputs[i];
+ if (input.parentElement.style.display !== "none") {
+ const t = this.templates[i];
+ t.name = input.value.trim() || input.getAttribute("data-name");
+ updated.push(t);
+ }
+ }
+
+ this.templates = updated;
+ this.store();
+ this.close();
+ }
+
+ store() {
+ localStorage.setItem(id, JSON.stringify(this.templates));
+ }
+
+ show() {
+ // Show list of template names + delete button
+ super.show(
+ $el(
+ "div",
+ {
+ style: {
+ display: "grid",
+ gridTemplateColumns: "1fr auto",
+ gap: "5px",
+ },
+ },
+ this.templates.flatMap((t) => {
+ let nameInput;
+ return [
+ $el(
+ "label",
+ {
+ textContent: "Name: ",
+ },
+ [
+ $el("input", {
+ value: t.name,
+ dataset: { name: t.name },
+ $: (el) => (nameInput = el),
+ }),
+ ]
+ ),
+ $el("button", {
+ textContent: "Delete",
+ style: {
+ fontSize: "12px",
+ color: "red",
+ fontWeight: "normal",
+ },
+ onclick: (e) => {
+ nameInput.value = "";
+ e.target.style.display = "none";
+ e.target.previousElementSibling.style.display = "none";
+ },
+ }),
+ ];
+ })
+ )
+ );
+ }
+}
+
+app.registerExtension({
+ name: id,
+ setup() {
+ const manage = new ManageTemplates();
+
+ const clipboardAction = (cb) => {
+ // We use the clipboard functions but dont want to overwrite the current user clipboard
+ // Restore it after we've run our callback
+ const old = localStorage.getItem("litegrapheditor_clipboard");
+ cb();
+ localStorage.setItem("litegrapheditor_clipboard", old);
+ };
+
+ const orig = LGraphCanvas.prototype.getCanvasMenuOptions;
+ LGraphCanvas.prototype.getCanvasMenuOptions = function () {
+ const options = orig.apply(this, arguments);
+
+ options.push(null);
+ options.push({
+ content: `Save Selected as Template`,
+ disabled: !Object.keys(app.canvas.selected_nodes || {}).length,
+ callback: () => {
+ const name = prompt("Enter name");
+ if (!name || !name.trim()) return;
+
+ clipboardAction(() => {
+ app.canvas.copyToClipboard();
+ manage.templates.push({
+ name,
+ data: localStorage.getItem("litegrapheditor_clipboard"),
+ });
+ manage.store();
+ });
+ },
+ });
+
+ // Map each template to a menu item
+ const subItems = manage.templates.map((t) => ({
+ content: t.name,
+ callback: () => {
+ clipboardAction(() => {
+ localStorage.setItem("litegrapheditor_clipboard", t.data);
+ app.canvas.pasteFromClipboard();
+ });
+ },
+ }));
+
+ if (subItems.length) {
+ subItems.push(null, {
+ content: "Manage",
+ callback: () => manage.show(),
+ });
+
+ options.push({
+ content: "Node Templates",
+ submenu: {
+ options: subItems,
+ },
+ });
+ }
+
+ return options;
+ };
+ },
+});
diff --git a/web/index.html b/web/index.html
index 86156a7f8..bb79433ce 100644
--- a/web/index.html
+++ b/web/index.html
@@ -2,6 +2,7 @@
+ ComfyUI
diff --git a/web/lib/litegraph.core.js b/web/lib/litegraph.core.js
index 862d59067..c3efa22a9 100644
--- a/web/lib/litegraph.core.js
+++ b/web/lib/litegraph.core.js
@@ -89,6 +89,7 @@
NO_TITLE: 1,
TRANSPARENT_TITLE: 2,
AUTOHIDE_TITLE: 3,
+ VERTICAL_LAYOUT: "vertical", // arrange nodes vertically
proxy: null, //used to redirect calls
node_images_path: "",
@@ -125,14 +126,14 @@
registered_slot_out_types: {}, // slot types for nodeclass
slot_types_in: [], // slot types IN
slot_types_out: [], // slot types OUT
- slot_types_default_in: [], // specify for each IN slot type a(/many) deafult node(s), use single string, array, or object (with node, title, parameters, ..) like for search
- slot_types_default_out: [], // specify for each OUT slot type a(/many) deafult node(s), use single string, array, or object (with node, title, parameters, ..) like for search
+ slot_types_default_in: [], // specify for each IN slot type a(/many) default node(s), use single string, array, or object (with node, title, parameters, ..) like for search
+ slot_types_default_out: [], // specify for each OUT slot type a(/many) default node(s), use single string, array, or object (with node, title, parameters, ..) like for search
alt_drag_do_clone_nodes: false, // [true!] very handy, ALT click to clone and drag the new node
do_add_triggers_slots: false, // [true!] will create and connect event slots when using action/events connections, !WILL CHANGE node mode when using onTrigger (enable mode colors), onExecuted does not need this
- allow_multi_output_for_events: true, // [false!] being events, it is strongly reccomended to use them sequentually, one by one
+ allow_multi_output_for_events: true, // [false!] being events, it is strongly reccomended to use them sequentially, one by one
middle_click_slot_add_default_node: false, //[true!] allows to create and connect a ndoe clicking with the third button (wheel)
@@ -158,80 +159,67 @@
console.log("Node registered: " + type);
}
- var categories = type.split("/");
- var classname = base_class.name;
+ const classname = base_class.name;
- var pos = type.lastIndexOf("/");
- base_class.category = type.substr(0, pos);
+ const pos = type.lastIndexOf("/");
+ base_class.category = type.substring(0, pos);
if (!base_class.title) {
base_class.title = classname;
}
- //info.name = name.substr(pos+1,name.length - pos);
//extend class
- if (base_class.prototype) {
- //is a class
- for (var i in LGraphNode.prototype) {
- if (!base_class.prototype[i]) {
- base_class.prototype[i] = LGraphNode.prototype[i];
- }
+ for (var i in LGraphNode.prototype) {
+ if (!base_class.prototype[i]) {
+ base_class.prototype[i] = LGraphNode.prototype[i];
}
}
- var prev = this.registered_node_types[type];
- if(prev)
- console.log("replacing node type: " + type);
- else
- {
- if( !Object.hasOwnProperty( base_class.prototype, "shape") )
- Object.defineProperty(base_class.prototype, "shape", {
- set: function(v) {
- switch (v) {
- case "default":
- delete this._shape;
- break;
- case "box":
- this._shape = LiteGraph.BOX_SHAPE;
- break;
- case "round":
- this._shape = LiteGraph.ROUND_SHAPE;
- break;
- case "circle":
- this._shape = LiteGraph.CIRCLE_SHAPE;
- break;
- case "card":
- this._shape = LiteGraph.CARD_SHAPE;
- break;
- default:
- this._shape = v;
- }
- },
- get: function(v) {
- return this._shape;
- },
- enumerable: true,
- configurable: true
- });
+ const prev = this.registered_node_types[type];
+ if(prev) {
+ console.log("replacing node type: " + type);
+ }
+ if( !Object.prototype.hasOwnProperty.call( base_class.prototype, "shape") ) {
+ Object.defineProperty(base_class.prototype, "shape", {
+ set: function(v) {
+ switch (v) {
+ case "default":
+ delete this._shape;
+ break;
+ case "box":
+ this._shape = LiteGraph.BOX_SHAPE;
+ break;
+ case "round":
+ this._shape = LiteGraph.ROUND_SHAPE;
+ break;
+ case "circle":
+ this._shape = LiteGraph.CIRCLE_SHAPE;
+ break;
+ case "card":
+ this._shape = LiteGraph.CARD_SHAPE;
+ break;
+ default:
+ this._shape = v;
+ }
+ },
+ get: function() {
+ return this._shape;
+ },
+ enumerable: true,
+ configurable: true
+ });
+
- //warnings
- if (base_class.prototype.onPropertyChange) {
- console.warn(
- "LiteGraph node class " +
- type +
- " has onPropertyChange method, it must be called onPropertyChanged with d at the end"
- );
- }
-
- //used to know which nodes create when dragging files to the canvas
- if (base_class.supported_extensions) {
- for (var i in base_class.supported_extensions) {
- var ext = base_class.supported_extensions[i];
- if(ext && ext.constructor === String)
- this.node_types_by_file_extension[ ext.toLowerCase() ] = base_class;
- }
- }
- }
+ //used to know which nodes to create when dragging files to the canvas
+ if (base_class.supported_extensions) {
+ for (let i in base_class.supported_extensions) {
+ const ext = base_class.supported_extensions[i];
+ if(ext && ext.constructor === String) {
+ this.node_types_by_file_extension[ ext.toLowerCase() ] = base_class;
+ }
+ }
+ }
+ }
this.registered_node_types[type] = base_class;
if (base_class.constructor.name) {
@@ -252,19 +240,11 @@
" has onPropertyChange method, it must be called onPropertyChanged with d at the end"
);
}
-
- //used to know which nodes create when dragging files to the canvas
- if (base_class.supported_extensions) {
- for (var i=0; i < base_class.supported_extensions.length; i++) {
- var ext = base_class.supported_extensions[i];
- if(ext && ext.constructor === String)
- this.node_types_by_file_extension[ ext.toLowerCase() ] = base_class;
- }
- }
- // TODO one would want to know input and ouput :: this would allow trought registerNodeAndSlotType to get all the slots types
- //console.debug("Registering "+type);
- if (this.auto_load_slot_types) nodeTmp = new base_class(base_class.title || "tmpnode");
+ // TODO one would want to know input and ouput :: this would allow through registerNodeAndSlotType to get all the slots types
+ if (this.auto_load_slot_types) {
+ new base_class(base_class.title || "tmpnode");
+ }
},
/**
@@ -1260,37 +1240,39 @@
* Positions every node in a more readable manner
* @method arrange
*/
- LGraph.prototype.arrange = function(margin) {
+ LGraph.prototype.arrange = function (margin, layout) {
margin = margin || 100;
- var nodes = this.computeExecutionOrder(false, true);
- var columns = [];
- for (var i = 0; i < nodes.length; ++i) {
- var node = nodes[i];
- var col = node._level || 1;
+ const nodes = this.computeExecutionOrder(false, true);
+ const columns = [];
+ for (let i = 0; i < nodes.length; ++i) {
+ const node = nodes[i];
+ const col = node._level || 1;
if (!columns[col]) {
columns[col] = [];
}
columns[col].push(node);
}
- var x = margin;
+ let x = margin;
- for (var i = 0; i < columns.length; ++i) {
- var column = columns[i];
+ for (let i = 0; i < columns.length; ++i) {
+ const column = columns[i];
if (!column) {
continue;
}
- var max_size = 100;
- var y = margin + LiteGraph.NODE_TITLE_HEIGHT;
- for (var j = 0; j < column.length; ++j) {
- var node = column[j];
- node.pos[0] = x;
- node.pos[1] = y;
- if (node.size[0] > max_size) {
- max_size = node.size[0];
+ let max_size = 100;
+ let y = margin + LiteGraph.NODE_TITLE_HEIGHT;
+ for (let j = 0; j < column.length; ++j) {
+ const node = column[j];
+ node.pos[0] = (layout == LiteGraph.VERTICAL_LAYOUT) ? y : x;
+ node.pos[1] = (layout == LiteGraph.VERTICAL_LAYOUT) ? x : y;
+ const max_size_index = (layout == LiteGraph.VERTICAL_LAYOUT) ? 1 : 0;
+ if (node.size[max_size_index] > max_size) {
+ max_size = node.size[max_size_index];
}
- y += node.size[1] + margin + LiteGraph.NODE_TITLE_HEIGHT;
+ const node_size_index = (layout == LiteGraph.VERTICAL_LAYOUT) ? 0 : 1;
+ y += node.size[node_size_index] + margin + LiteGraph.NODE_TITLE_HEIGHT;
}
x += max_size + margin;
}
@@ -2468,43 +2450,34 @@
this.title = this.constructor.title;
}
- if (this.onConnectionsChange) {
- if (this.inputs) {
- for (var i = 0; i < this.inputs.length; ++i) {
- var input = this.inputs[i];
- var link_info = this.graph
- ? this.graph.links[input.link]
- : null;
- this.onConnectionsChange(
- LiteGraph.INPUT,
- i,
- true,
- link_info,
- input
- ); //link_info has been created now, so its updated
- }
- }
+ if (this.inputs) {
+ for (var i = 0; i < this.inputs.length; ++i) {
+ var input = this.inputs[i];
+ var link_info = this.graph ? this.graph.links[input.link] : null;
+ if (this.onConnectionsChange)
+ this.onConnectionsChange( LiteGraph.INPUT, i, true, link_info, input ); //link_info has been created now, so its updated
- if (this.outputs) {
- for (var i = 0; i < this.outputs.length; ++i) {
- var output = this.outputs[i];
- if (!output.links) {
- continue;
- }
- for (var j = 0; j < output.links.length; ++j) {
- var link_info = this.graph
- ? this.graph.links[output.links[j]]
- : null;
- this.onConnectionsChange(
- LiteGraph.OUTPUT,
- i,
- true,
- link_info,
- output
- ); //link_info has been created now, so its updated
- }
- }
- }
+ if( this.onInputAdded )
+ this.onInputAdded(input);
+
+ }
+ }
+
+ if (this.outputs) {
+ for (var i = 0; i < this.outputs.length; ++i) {
+ var output = this.outputs[i];
+ if (!output.links) {
+ continue;
+ }
+ for (var j = 0; j < output.links.length; ++j) {
+ var link_info = this.graph ? this.graph.links[output.links[j]] : null;
+ if (this.onConnectionsChange)
+ this.onConnectionsChange( LiteGraph.OUTPUT, i, true, link_info, output ); //link_info has been created now, so its updated
+ }
+
+ if( this.onOutputAdded )
+ this.onOutputAdded(output);
+ }
}
if( this.widgets )
@@ -3200,6 +3173,15 @@
return;
}
+ if(slot == null)
+ {
+ console.error("slot must be a number");
+ return;
+ }
+
+ if(slot.constructor !== Number)
+ console.warn("slot must be a number, use node.trigger('name') if you want to use a string");
+
var output = this.outputs[slot];
if (!output) {
return;
@@ -3346,26 +3328,26 @@
* @param {Object} extra_info this can be used to have special properties of an output (label, special color, position, etc)
*/
LGraphNode.prototype.addOutput = function(name, type, extra_info) {
- var o = { name: name, type: type, links: null };
+ var output = { name: name, type: type, links: null };
if (extra_info) {
for (var i in extra_info) {
- o[i] = extra_info[i];
+ output[i] = extra_info[i];
}
}
if (!this.outputs) {
this.outputs = [];
}
- this.outputs.push(o);
+ this.outputs.push(output);
if (this.onOutputAdded) {
- this.onOutputAdded(o);
+ this.onOutputAdded(output);
}
if (LiteGraph.auto_load_slot_types) LiteGraph.registerNodeAndSlotType(this,type,true);
this.setSize( this.computeSize() );
this.setDirtyCanvas(true, true);
- return o;
+ return output;
};
/**
@@ -3437,10 +3419,10 @@
*/
LGraphNode.prototype.addInput = function(name, type, extra_info) {
type = type || 0;
- var o = { name: name, type: type, link: null };
+ var input = { name: name, type: type, link: null };
if (extra_info) {
for (var i in extra_info) {
- o[i] = extra_info[i];
+ input[i] = extra_info[i];
}
}
@@ -3448,17 +3430,17 @@
this.inputs = [];
}
- this.inputs.push(o);
+ this.inputs.push(input);
this.setSize( this.computeSize() );
if (this.onInputAdded) {
- this.onInputAdded(o);
+ this.onInputAdded(input);
}
LiteGraph.registerNodeAndSlotType(this,type);
this.setDirtyCanvas(true, true);
- return o;
+ return input;
};
/**
@@ -5210,6 +5192,7 @@ LGraphNode.prototype.executeAction = function(action)
this.allow_dragcanvas = true;
this.allow_dragnodes = true;
this.allow_interaction = true; //allow to control widgets, buttons, collapse, etc
+ this.multi_select = false; //allow selecting multi nodes without pressing extra keys
this.allow_searchbox = true;
this.allow_reconnect_links = true; //allows to change a connection with having to redo it again
this.align_to_grid = false; //snap to grid
@@ -5435,7 +5418,7 @@ LGraphNode.prototype.executeAction = function(action)
};
/**
- * returns the visualy active graph (in case there are more in the stack)
+ * returns the visually active graph (in case there are more in the stack)
* @method getCurrentGraph
* @return {LGraph} the active graph
*/
@@ -6060,9 +6043,13 @@ LGraphNode.prototype.executeAction = function(action)
this.graph.beforeChange();
this.node_dragged = node;
}
- if (!this.selected_nodes[node.id]) {
- this.processNodeSelected(node, e);
- }
+ this.processNodeSelected(node, e);
+ } else { // double-click
+ /**
+ * Don't call the function if the block is already selected.
+ * Otherwise, it could cause the block to be unselected while its panel is open.
+ */
+ if (!node.is_selected) this.processNodeSelected(node, e);
}
this.dirty_canvas = true;
@@ -6474,6 +6461,10 @@ LGraphNode.prototype.executeAction = function(action)
var n = this.selected_nodes[i];
n.pos[0] += delta[0] / this.ds.scale;
n.pos[1] += delta[1] / this.ds.scale;
+ if (!n.is_selected) this.processNodeSelected(n, e); /*
+ * Don't call the function if the block is already selected.
+ * Otherwise, it could cause the block to be unselected while dragging.
+ */
}
this.dirty_canvas = true;
@@ -7287,7 +7278,7 @@ LGraphNode.prototype.executeAction = function(action)
};
LGraphCanvas.prototype.processNodeSelected = function(node, e) {
- this.selectNode(node, e && (e.shiftKey||e.ctrlKey));
+ this.selectNode(node, e && (e.shiftKey || e.ctrlKey || this.multi_select));
if (this.onNodeSelected) {
this.onNodeSelected(node);
}
@@ -7323,6 +7314,7 @@ LGraphNode.prototype.executeAction = function(action)
for (var i in nodes) {
var node = nodes[i];
if (node.is_selected) {
+ this.deselectNode(node);
continue;
}
@@ -9742,13 +9734,17 @@ LGraphNode.prototype.executeAction = function(action)
ctx.fillRect(margin, y, widget_width - margin * 2, H);
var range = w.options.max - w.options.min;
var nvalue = (w.value - w.options.min) / range;
- ctx.fillStyle = active_widget == w ? "#89A" : "#678";
+ if(nvalue < 0.0) nvalue = 0.0;
+ if(nvalue > 1.0) nvalue = 1.0;
+ ctx.fillStyle = w.options.hasOwnProperty("slider_color") ? w.options.slider_color : (active_widget == w ? "#89A" : "#678");
ctx.fillRect(margin, y, nvalue * (widget_width - margin * 2), H);
if(show_text && !w.disabled)
ctx.strokeRect(margin, y, widget_width - margin * 2, H);
if (w.marker) {
var marker_nvalue = (w.marker - w.options.min) / range;
- ctx.fillStyle = "#AA9";
+ if(marker_nvalue < 0.0) marker_nvalue = 0.0;
+ if(marker_nvalue > 1.0) marker_nvalue = 1.0;
+ ctx.fillStyle = w.options.hasOwnProperty("marker_color") ? w.options.marker_color : "#AA9";
ctx.fillRect( margin + marker_nvalue * (widget_width - margin * 2), y, 2, H );
}
if (show_text) {
@@ -9915,6 +9911,7 @@ LGraphNode.prototype.executeAction = function(action)
case "slider":
var range = w.options.max - w.options.min;
var nvalue = Math.clamp((x - 15) / (widget_width - 30), 0, 1);
+ if(w.options.read_only) break;
w.value = w.options.min + (w.options.max - w.options.min) * nvalue;
if (w.callback) {
setTimeout(function() {
@@ -9926,8 +9923,16 @@ LGraphNode.prototype.executeAction = function(action)
case "number":
case "combo":
var old_value = w.value;
- if (event.type == LiteGraph.pointerevents_method+"move" && w.type == "number") {
- w.value += event.deltaX * 0.1 * (w.options.step || 1);
+ var delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0;
+ var allow_scroll = true;
+ if (delta) {
+ if (x > -3 && x < widget_width + 3) {
+ allow_scroll = false;
+ }
+ }
+ if (allow_scroll && event.type == LiteGraph.pointerevents_method+"move" && w.type == "number") {
+ if(event.deltaX)
+ w.value += event.deltaX * 0.1 * (w.options.step || 1);
if ( w.options.min != null && w.value < w.options.min ) {
w.value = w.options.min;
}
@@ -9994,6 +9999,12 @@ LGraphNode.prototype.executeAction = function(action)
var delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0;
if (event.click_time < 200 && delta == 0) {
this.prompt("Value",w.value,function(v) {
+ // check if v is a valid equation or a number
+ if (/^[0-9+\-*/()\s]+$/.test(v)) {
+ try {//solve the equation if possible
+ v = eval(v);
+ } catch (e) { }
+ }
this.value = Number(v);
inner_value_change(this, this.value);
}.bind(w),
@@ -10022,7 +10033,6 @@ LGraphNode.prototype.executeAction = function(action)
case "text":
if (event.type == LiteGraph.pointerevents_method+"down") {
this.prompt("Value",w.value,function(v) {
- this.value = v;
inner_value_change(this, v);
}.bind(w),
event,w.options ? w.options.multiline : false );
@@ -10047,6 +10057,9 @@ LGraphNode.prototype.executeAction = function(action)
}//end for
function inner_value_change(widget, value) {
+ if(widget.type == "number"){
+ value = Number(value);
+ }
widget.value = value;
if ( widget.options && widget.options.property && node.properties[widget.options.property] !== undefined ) {
node.setProperty( widget.options.property, value );
@@ -11165,7 +11178,7 @@ LGraphNode.prototype.executeAction = function(action)
LGraphCanvas.search_limit = -1;
LGraphCanvas.prototype.showSearchBox = function(event, options) {
// proposed defaults
- def_options = { slot_from: null
+ var def_options = { slot_from: null
,node_from: null
,node_to: null
,do_type_filter: LiteGraph.search_filter_enabled // TODO check for registered_slot_[in/out]_types not empty // this will be checked for functionality enabled : filter on slot type, in and out
@@ -11863,7 +11876,7 @@ LGraphNode.prototype.executeAction = function(action)
// TODO refactor, theer are different dialog, some uses createDialog, some dont
LGraphCanvas.prototype.createDialog = function(html, options) {
- def_options = { checkForInput: false, closeOnLeave: true, closeOnLeave_checkModified: true };
+ var def_options = { checkForInput: false, closeOnLeave: true, closeOnLeave_checkModified: true };
options = Object.assign(def_options, options || {});
var dialog = document.createElement("div");
@@ -11993,7 +12006,8 @@ LGraphNode.prototype.executeAction = function(action)
if (root.onClose && typeof root.onClose == "function"){
root.onClose();
}
- root.parentNode.removeChild(root);
+ if(root.parentNode)
+ root.parentNode.removeChild(root);
/* XXX CHECK THIS */
if(this.parentNode){
this.parentNode.removeChild(this);
@@ -12285,7 +12299,7 @@ LGraphNode.prototype.executeAction = function(action)
var ref_window = this.getCanvasWindow();
var that = this;
var graphcanvas = this;
- panel = this.createPanel(node.title || "",{
+ var panel = this.createPanel(node.title || "",{
closable: true
,window: ref_window
,onOpen: function(){
diff --git a/web/scripts/app.js b/web/scripts/app.js
index a687eb5c2..0399ac722 100644
--- a/web/scripts/app.js
+++ b/web/scripts/app.js
@@ -1,5 +1,5 @@
import { ComfyWidgets } from "./widgets.js";
-import { ComfyUI } from "./ui.js";
+import { ComfyUI, $el } from "./ui.js";
import { api } from "./api.js";
import { defaultGraph } from "./defaultGraph.js";
import { getPngMetadata, importA1111 } from "./pnginfo.js";
@@ -835,7 +835,7 @@ class ComfyApp {
app.#invokeExtensionsAsync("nodeCreated", this);
},
{
- title: nodeData.name,
+ title: nodeData.display_name || nodeData.name,
comfyClass: nodeData.name,
}
);
@@ -864,12 +864,62 @@ class ComfyApp {
graphData = structuredClone(defaultGraph);
}
- // Patch T2IAdapterLoader to ControlNetLoader since they are the same node now
+ const missingNodeTypes = [];
for (let n of graphData.nodes) {
+ // Patch T2IAdapterLoader to ControlNetLoader since they are the same node now
if (n.type == "T2IAdapterLoader") n.type = "ControlNetLoader";
+
+ // Find missing node types
+ if (!(n.type in LiteGraph.registered_node_types)) {
+ missingNodeTypes.push(n.type);
+ }
}
- this.graph.configure(graphData);
+ try {
+ this.graph.configure(graphData);
+ } catch (error) {
+ let errorHint = [];
+ // Try extracting filename to see if it was caused by an extension script
+ const filename = error.fileName || (error.stack || "").match(/(\/extensions\/.*\.js)/)?.[1];
+ const pos = (filename || "").indexOf("/extensions/");
+ if (pos > -1) {
+ errorHint.push(
+ $el("span", { textContent: "This may be due to the following script:" }),
+ $el("br"),
+ $el("span", {
+ style: {
+ fontWeight: "bold",
+ },
+ textContent: filename.substring(pos),
+ })
+ );
+ }
+
+ // Show dialog to let the user know something went wrong loading the data
+ this.ui.dialog.show(
+ $el("div", [
+ $el("p", { textContent: "Loading aborted due to error reloading workflow data" }),
+ $el("pre", {
+ style: { padding: "5px", backgroundColor: "rgba(255,0,0,0.2)" },
+ textContent: error.toString(),
+ }),
+ $el("pre", {
+ style: {
+ padding: "5px",
+ color: "#ccc",
+ fontSize: "10px",
+ maxHeight: "50vh",
+ overflow: "auto",
+ backgroundColor: "rgba(0,0,0,0.2)",
+ },
+ textContent: error.stack || "No stacktrace available",
+ }),
+ ...errorHint,
+ ]).outerHTML
+ );
+
+ return;
+ }
for (const node of this.graph._nodes) {
const size = node.computeSize();
@@ -893,6 +943,14 @@ class ComfyApp {
this.#invokeExtensions("loadedGraphNode", node);
}
+
+ if (missingNodeTypes.length) {
+ this.ui.dialog.show(
+ `When loading the graph, the following node types were not found: ${Array.from(new Set(missingNodeTypes)).map(
+ (t) => `- ${t}
`
+ ).join("")}
Nodes that have failed to load will show as red on the graph.`
+ );
+ }
}
/**
diff --git a/web/scripts/defaultGraph.js b/web/scripts/defaultGraph.js
index 967377ad6..9b3cb4a7e 100644
--- a/web/scripts/defaultGraph.js
+++ b/web/scripts/defaultGraph.js
@@ -13,7 +13,7 @@ export const defaultGraph = {
inputs: [{ name: "clip", type: "CLIP", link: 5 }],
outputs: [{ name: "CONDITIONING", type: "CONDITIONING", links: [6], slot_index: 0 }],
properties: {},
- widgets_values: ["bad hands"],
+ widgets_values: ["text, watermark"],
},
{
id: 6,
@@ -26,7 +26,7 @@ export const defaultGraph = {
inputs: [{ name: "clip", type: "CLIP", link: 3 }],
outputs: [{ name: "CONDITIONING", type: "CONDITIONING", links: [4], slot_index: 0 }],
properties: {},
- widgets_values: ["masterpiece best quality girl"],
+ widgets_values: ["beautiful scenery nature glass bottle landscape, , purple galaxy bottle,"],
},
{
id: 5,
@@ -56,7 +56,7 @@ export const defaultGraph = {
],
outputs: [{ name: "LATENT", type: "LATENT", links: [7], slot_index: 0 }],
properties: {},
- widgets_values: [8566257, true, 20, 8, "euler", "normal", 1],
+ widgets_values: [156680208700286, true, 20, 8, "euler", "normal", 1],
},
{
id: 8,
diff --git a/web/scripts/pnginfo.js b/web/scripts/pnginfo.js
index 580030d81..31f470739 100644
--- a/web/scripts/pnginfo.js
+++ b/web/scripts/pnginfo.js
@@ -32,8 +32,9 @@ export function getPngMetadata(file) {
}
const keyword = String.fromCharCode(...pngData.slice(offset + 8, keyword_end));
// Get the text
- const text = String.fromCharCode(...pngData.slice(keyword_end + 1, offset + 8 + length));
- txt_chunks[keyword] = text;
+ const contentArraySegment = pngData.slice(keyword_end + 1, offset + 8 + length);
+ const contentJson = Array.from(contentArraySegment).map(s=>String.fromCharCode(s)).join('')
+ txt_chunks[keyword] = contentJson;
}
offset += 12 + length;
diff --git a/web/scripts/ui.js b/web/scripts/ui.js
index 6999c0a73..09861c440 100644
--- a/web/scripts/ui.js
+++ b/web/scripts/ui.js
@@ -8,14 +8,18 @@ export function $el(tag, propsOrChildren, children) {
if (Array.isArray(propsOrChildren)) {
element.append(...propsOrChildren);
} else {
- const parent = propsOrChildren.parent;
+ const { parent, $: cb, dataset, style } = propsOrChildren;
delete propsOrChildren.parent;
- const cb = propsOrChildren.$;
delete propsOrChildren.$;
+ delete propsOrChildren.dataset;
+ delete propsOrChildren.style;
- if (propsOrChildren.style) {
- Object.assign(element.style, propsOrChildren.style);
- delete propsOrChildren.style;
+ if (style) {
+ Object.assign(element.style, style);
+ }
+
+ if (dataset) {
+ Object.assign(element.dataset, dataset);
}
Object.assign(element, propsOrChildren);
@@ -76,7 +80,7 @@ function dragElement(dragEl, settings) {
dragEl.style.left = newPosX + "px";
dragEl.style.right = "unset";
}
-
+
dragEl.style.top = newPosY + "px";
dragEl.style.bottom = "unset";
@@ -145,7 +149,7 @@ function dragElement(dragEl, settings) {
}
window.addEventListener("resize", () => {
- ensureInBounds();
+ ensureInBounds();
});
function closeDragElement() {
@@ -155,26 +159,33 @@ function dragElement(dragEl, settings) {
}
}
-class ComfyDialog {
+export class ComfyDialog {
constructor() {
this.element = $el("div.comfy-modal", { parent: document.body }, [
- $el("div.comfy-modal-content", [
- $el("p", { $: (p) => (this.textElement = p) }),
- $el("button", {
- type: "button",
- textContent: "Close",
- onclick: () => this.close(),
- }),
- ]),
+ $el("div.comfy-modal-content", [$el("p", { $: (p) => (this.textElement = p) }), ...this.createButtons()]),
]);
}
+ createButtons() {
+ return [
+ $el("button", {
+ type: "button",
+ textContent: "Close",
+ onclick: () => this.close(),
+ }),
+ ];
+ }
+
close() {
this.element.style.display = "none";
}
show(html) {
- this.textElement.innerHTML = html;
+ if (typeof html === "string") {
+ this.textElement.innerHTML = html;
+ } else {
+ this.textElement.replaceChildren(html);
+ }
this.element.style.display = "flex";
}
}
@@ -419,7 +430,7 @@ export class ComfyUI {
type: "boolean",
defaultValue: true,
});
-
+
const fileInput = $el("input", {
type: "file",
accept: ".json,image/png",
diff --git a/web/style.css b/web/style.css
index 27bb83bb3..d00a2fbe2 100644
--- a/web/style.css
+++ b/web/style.css
@@ -202,7 +202,8 @@ button.comfy-queue-btn {
margin: 6px 0 !important;
}
-.comfy-modal.comfy-settings {
+.comfy-modal.comfy-settings,
+.comfy-modal.comfy-manage-templates {
text-align: center;
font-family: sans-serif;
color: #999;
@@ -256,3 +257,16 @@ button.comfy-queue-btn {
color: #ddd;
border-radius: 12px 0 0 12px;
}
+
+.litegraph .litemenu-entry.has_submenu {
+ position: relative;
+ padding-right: 20px;
+ }
+
+ .litemenu-entry.has_submenu::after {
+ content: ">";
+ position: absolute;
+ top: 0;
+ right: 2px;
+ }
+
\ No newline at end of file