diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index 2ae57ba..2659208 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -58,10 +58,10 @@ module.exports = {
// false positive https://github.com/typescript-eslint/typescript-eslint/issues/2483
'no-shadow': 'off',
'object-curly-newline': ["error", {
- "ObjectExpression": { "multiline": true, "minProperties": 1 },
- "ObjectPattern": { "multiline": true, "minProperties": 1 },
+ "ObjectExpression": { "multiline": true, "minProperties": 8 },
+ "ObjectPattern": { "multiline": true, "minProperties": 8 },
"ImportDeclaration": {"multiline": true, "minProperties": 8 },
- "ExportDeclaration": { "multiline": true, "minProperties": 1 }
+ "ExportDeclaration": { "multiline": true, "minProperties": 8 }
}],
"@typescript-eslint/explicit-function-return-type": 'error',
'@typescript-eslint/triple-slash-reference': 'off',
diff --git a/.others/ComfyUI.run.xml b/.others/ComfyUI.run.xml
index 805dbaa..8aef141 100644
--- a/.others/ComfyUI.run.xml
+++ b/.others/ComfyUI.run.xml
@@ -9,13 +9,12 @@
-
-
-
+
+
-
+
diff --git a/.others/pre-commit b/.others/pre-commit
index ca982f6..82b182b 100644
--- a/.others/pre-commit
+++ b/.others/pre-commit
@@ -1,7 +1,7 @@
#!/bin/bash
# REMEMBER TO COPY THIS FILE INTO .git/hooks/pre-commit
-cd ../comfyUI/ComfyUI
+cd ../comfy/ComfyUI
rm ./custom_nodes/ComfyUI-Crystools/web/*.js -f
LOCAL_COMMAND="npm --prefix ./custom_nodes/ComfyUI-Crystools run validate"
diff --git a/.others/typescript.run.xml b/.others/typescript.run.xml
index e2505af..100a76b 100644
--- a/.others/typescript.run.xml
+++ b/.others/typescript.run.xml
@@ -1,11 +1,11 @@
-
+
-
+
diff --git a/README.md b/README.md
index c57a501..413eb5a 100644
--- a/README.md
+++ b/README.md
@@ -27,7 +27,11 @@ This provides better nodes to load/save images, previews, etc, and see "hidden"
**🎉Finally, you can see the resources used by ComfyUI (CPU, GPU, RAM, VRAM, GPU Temp and space) on the menu in real-time!**
-
+Horizontal:
+
+
+Vertical:
+
Now you can identify the bottlenecks in your workflow and know when it's time to restart the server, unload models or even close some tabs!
@@ -512,6 +516,21 @@ You have predefined switches (string, latent, image, conditioning) but you can u
### Crystools
+### 1.20.0 (21/10/2024)
+- BETA of JSON file reader and extractor, to allow you to read your own JSON files and extract the values to use in your workflow
+
+### 1.19.0 (06/10/2024)
+- HORIZONTAL UI! New version is ready! 🎉
+
+### 1.18.0 (21/09/2024)
+- HORIZONTAL UI! 🎉
+- Configurable size of monitors on settings menu
+
+### 1.17.0 (21/09/2024)
+- Settings menu reorganized
+- Preparing for horizontal UI
+- Update from ComfyUI (typescript and new features)
+
### 1.16.0 (31/07/2024)
- Rollback of AMD support by manager does not support other repository parameter (https://test.pypi.org/simple by pyrsmi)
diff --git a/__init__.py b/__init__.py
index 2ea2d8e..bf011b7 100644
--- a/__init__.py
+++ b/__init__.py
@@ -2,7 +2,7 @@
@author: Crystian
@title: Crystools
@nickname: Crystools
-@version: 1.16.6
+@version: 1.20.0
@project: "https://github.com/crystian/ComfyUI-Crystools",
@description: Plugins for multiples uses, mainly for debugging, you need them! IG: https://www.instagram.com/crystian.ia
"""
@@ -21,6 +21,7 @@ from .nodes.list import CListAny, CListString
from .nodes.pipe import CPipeToAny, CPipeFromAny
from .nodes.utils import CUtilsCompareJsons, CUtilsStatSystem
from .nodes.metadata import CMetadataExtractor, CMetadataCompare
+from .nodes.parameters import CJsonFile, CJsonExtractor
from .server import *
from .general import *
@@ -56,7 +57,9 @@ NODE_CLASS_MAPPINGS = {
CLASSES.CMETADATA_EXTRACTOR_NAME.value: CMetadataExtractor,
CLASSES.CMETADATA_COMPARATOR_NAME.value: CMetadataCompare,
CLASSES.CUTILS_JSON_COMPARATOR_NAME.value: CUtilsCompareJsons,
- CLASSES.CUTILS_STAT_SYSTEM_NAME.value: CUtilsStatSystem
+ CLASSES.CUTILS_STAT_SYSTEM_NAME.value: CUtilsStatSystem,
+ CLASSES.CJSONFILE_NAME.value: CJsonFile,
+ CLASSES.CJSONEXTRACTOR_NAME.value: CJsonExtractor,
}
NODE_DISPLAY_NAME_MAPPINGS = {
@@ -93,6 +96,9 @@ NODE_DISPLAY_NAME_MAPPINGS = {
CLASSES.CUTILS_JSON_COMPARATOR_NAME.value: CLASSES.CUTILS_JSON_COMPARATOR_DESC.value,
CLASSES.CUTILS_STAT_SYSTEM_NAME.value: CLASSES.CUTILS_STAT_SYSTEM_DESC.value,
+
+ CLASSES.CJSONFILE_NAME.value: CLASSES.CJSONFILE_DESC.value,
+ CLASSES.CJSONEXTRACTOR_NAME.value: CLASSES.CJSONEXTRACTOR_DESC.value,
}
diff --git a/core/common.py b/core/common.py
index 5b257ce..f963b71 100644
--- a/core/common.py
+++ b/core/common.py
@@ -105,3 +105,13 @@ def get_size(path):
return f"{round(size / (pow(1024, 2)), 2)} MB"
elif size < pow(1024, 4):
return f"{round(size / (pow(1024, 3)), 2)} GB"
+
+
+def get_nested_value(data, dotted_key, default=None):
+ keys = dotted_key.split('.')
+ for key in keys:
+ if isinstance(data, dict) and key in data:
+ data = data[key]
+ else:
+ return default
+ return data
diff --git a/core/version.py b/core/version.py
index 3e213d1..8e84480 100644
--- a/core/version.py
+++ b/core/version.py
@@ -1 +1 @@
-version = "1.16.6"
+version = "1.20.0"
diff --git a/docs/monitor.webp b/docs/monitor.webp
deleted file mode 100644
index 25d0f0c..0000000
Binary files a/docs/monitor.webp and /dev/null differ
diff --git a/docs/monitor1.webp b/docs/monitor1.webp
new file mode 100644
index 0000000..d8359a8
Binary files /dev/null and b/docs/monitor1.webp differ
diff --git a/docs/monitor3.webp b/docs/monitor3.webp
new file mode 100644
index 0000000..f45bc84
Binary files /dev/null and b/docs/monitor3.webp differ
diff --git a/general/gpu.py b/general/gpu.py
index f14d2c6..786a3af 100644
--- a/general/gpu.py
+++ b/general/gpu.py
@@ -110,11 +110,11 @@ class CGPUInfo:
if self.cudaDevice == 'cpu':
gpuType = 'cpu'
gpus.append({
- 'gpu_utilization': 0,
- 'gpu_temperature': 0,
- 'vram_total': 0,
- 'vram_used': 0,
- 'vram_used_percent': 0,
+ 'gpu_utilization': -1,
+ 'gpu_temperature': -1,
+ 'vram_total': -1,
+ 'vram_used': -1,
+ 'vram_used_percent': -1,
})
else:
gpuType = self.cudaDevice
@@ -126,11 +126,11 @@ class CGPUInfo:
for deviceIndex in range(self.cudaDevicesFound):
deviceHandle = self.deviceGetHandleByIndex(deviceIndex)
- gpuUtilization = 0
- vramPercent = 0
- vramUsed = 0
- vramTotal = 0
- gpuTemperature = 0
+ gpuUtilization = -1
+ vramPercent = -1
+ vramUsed = -1
+ vramTotal = -1
+ gpuTemperature = -1
# GPU Utilization
if self.switchGPU and self.gpusUtilization[deviceIndex]:
diff --git a/nodes/_names.py b/nodes/_names.py
index 958e2cf..eef1b4c 100644
--- a/nodes/_names.py
+++ b/nodes/_names.py
@@ -63,7 +63,16 @@ class CLASSES(Enum):
CMETADATA_COMPARATOR_NAME = 'Metadata comparator [Crystools]'
CMETADATA_COMPARATOR_DESC = prefix + 'Metadata comparator'
- CUTILS_JSON_COMPARATOR_NAME = 'Json comparator [Crystools]'
- CUTILS_JSON_COMPARATOR_DESC = prefix + 'Json comparator'
+ CUTILS_JSON_COMPARATOR_NAME = 'JSON comparator [Crystools]'
+ CUTILS_JSON_COMPARATOR_DESC = prefix + 'JSON comparator'
CUTILS_STAT_SYSTEM_NAME = 'Stats system [Crystools]'
CUTILS_STAT_SYSTEM_DESC = prefix + 'Stats system (powered by WAS)'
+
+ # CPARAMETERS_NAME = 'External parameter from JSON file [Crystools]'
+ # CPARAMETERS_DESC = prefix + 'External parameters from JSON file'
+
+ CJSONFILE_NAME = 'Read JSON file [Crystools]'
+ CJSONFILE_DESC = prefix + 'Read JSON file (BETA)'
+
+ CJSONEXTRACTOR_NAME = 'JSON extractor [Crystools]'
+ CJSONEXTRACTOR_DESC = prefix + 'JSON extractor (BETA)'
diff --git a/nodes/parameters.py b/nodes/parameters.py
new file mode 100644
index 0000000..2bda391
--- /dev/null
+++ b/nodes/parameters.py
@@ -0,0 +1,170 @@
+import json
+from ..core import CONFIG, any, JSON_WIDGET, CATEGORY, STRING, INT, FLOAT, BOOLEAN, logger, get_nested_value
+
+# class CParameter:
+# def __init__(self):
+# pass
+#
+# @classmethod
+# def INPUT_TYPES(cls):
+# return {
+# "required": {
+# },
+# "optional": {
+# "path_to_json": STRING,
+# "key": STRING,
+# "default": STRING,
+# },
+# }
+#
+# CATEGORY = CATEGORY.MAIN.value + CATEGORY.UTILS.value
+# INPUT_IS_LIST = False
+#
+# RETURN_TYPES = (any,)
+# RETURN_NAMES = ("any",)
+#
+# FUNCTION = "execute"
+#
+# def execute(self, path_to_json=None, key=True, default=None):
+# text = default
+# value = text
+#
+# if path_to_json is not None and path_to_json != "":
+# logger.debug(f"External parameter from: '{path_to_json}'")
+# try:
+# with open(path_to_json, 'r') as file:
+# data = json.load(file)
+# logger.debug(f"File found, data: '{data}'")
+#
+# result = get_value(data, key, default)
+# text = result["text"]
+# value = result["value"]
+#
+# except Exception as e:
+# logger.error(e)
+# text = f"Error reading file: {e}\nReturning default value: '{default}'"
+# value = default
+#
+# return {"ui": {"text": [text]}, "result": [value]}
+
+class CJsonFile:
+ def __init__(self):
+ pass
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {
+ "required": {
+ },
+ "optional": {
+ "path_to_json": STRING,
+ },
+ }
+
+ CATEGORY = CATEGORY.MAIN.value + CATEGORY.UTILS.value
+ INPUT_IS_LIST = False
+
+ RETURN_TYPES = ("JSON",)
+ RETURN_NAMES = ("json",)
+
+ FUNCTION = "execute"
+
+ def IS_CHANGED(path_to_json=None):
+ return True
+
+ def execute(self, path_to_json=None):
+ text = ""
+ data = {}
+
+ if path_to_json is not None and path_to_json != "":
+ logger.debug(f"Open json file: '{path_to_json}'")
+ try:
+ with open(path_to_json, 'r') as file:
+ data = json.load(file)
+ text = json.dumps(data, indent=CONFIG["indent"])
+ logger.debug(f"File found, data: '{str(data)}'")
+
+ except Exception as e:
+ logger.error(e)
+ text = f"Error reading file: {e}"
+
+ return {"ui": {"text": [text]}, "result": [data]}
+
+class CJsonExtractor:
+ def __init__(self):
+ pass
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {
+ "required": {
+ "json": JSON_WIDGET,
+ },
+ "optional": {
+ "key": STRING,
+ "default": STRING,
+ },
+ }
+
+ CATEGORY = CATEGORY.MAIN.value + CATEGORY.UTILS.value
+ INPUT_IS_LIST = False
+
+ RETURN_TYPES = (any, "STRING", "INT", "FLOAT", "BOOLEAN")
+ RETURN_NAMES = ("any", "string", "int", "float", "boolean")
+
+ # OUTPUT_IS_LIST = (False,)
+
+ FUNCTION = "execute"
+
+ def execute(cls, json=None, key=True, default=None):
+ result = get_value(json, key, default)
+
+ result["any"] = result["value"]
+ try:
+ result["string"] = str(result["value"])
+ except Exception as e:
+ result["string"] = result["value"]
+
+ try:
+ result["int"] = int(result["value"])
+ except Exception as e:
+ result["int"] = result["value"]
+
+ try:
+ result["float"] = float(result["value"])
+ except Exception as e:
+ result["float"] = result["value"]
+
+ try:
+ result["boolean"] = result["value"].lower() == "true"
+ except Exception as e:
+ result["boolean"] = result["value"]
+
+ return {
+ "ui": {"text": [result["text"]]},
+ "result": [
+ result["any"],
+ result["string"],
+ result["int"],
+ result["float"],
+ result["boolean"]
+ ]
+ }
+
+def get_value(data, key, default=None):
+ text = ""
+ val = ""
+
+ if key is not None and key != "":
+ val = get_nested_value(data, key, default)
+ if default != val:
+ text = f"Key found, return value: '{val}'"
+ else:
+ text = f"Key no found, return default value: '{val}'"
+ else:
+ text = f"Key is empty, return default value: '{val}'"
+
+ return {
+ "text": text,
+ "value": val
+ }
diff --git a/pyproject.toml b/pyproject.toml
index eab91a3..fcf8cb1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,7 +1,7 @@
[project]
name = "comfyui-crystools"
description = "With this suit, you can see the resources monitor, progress bar & time elapsed, metadata and compare between two images, compare between two JSONs, show any value to console/display, pipes, and more!\nThis provides better nodes to load/save images, previews, etc, and see \"hidden\" data without loading a new workflow."
-version = "1.16.6"
+version = "1.20.0"
license = { file = "LICENSE" }
dependencies = ["deepdiff", "torch", "numpy", "Pillow", "pynvml", "py-cpuinfo"]
diff --git a/requirements.txt b/requirements.txt
index cd1f8a4..d61acb5 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,7 @@
deepdiff
torch
numpy
-Pillow
+Pillow>=9.5.0
pynvml
py-cpuinfo
piexif
diff --git a/tsconfig.json b/tsconfig.json
index 69b378f..8ccd4eb 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -6,19 +6,16 @@
"typeRoots": [
// "./web/ts/typings",
],
- "baseUrl": ".",
+// "rootDir": "../",
+ "baseUrl": "../../web",
+// "baseUrl": "./",
"paths": {
- "/scripts/*": ["../../web/scripts/*"],
- "/lib/*": ["../../web/lib/*"],
- "/extensions/*": ["../../web/extensions/*"],
- "/types/*": ["../../web/types/*"]
+// "/scripts/*": ["../../web/scripts/*"], // works
},
- // "rootDir": "./web/src",
- // "rootDir": "../../",
- // "rootDirs": [
- // "../../web",
- // "web/src"
- // ],
+ "rootDirs": [
+ "../../web",
+// "web"
+ ],
"preserveSymlinks": true,
"checkJs": false,
"allowJs": true,
@@ -58,7 +55,11 @@
"noEmit": false
},
"include": [
- "./web/*.ts"
+// "./custom_nodes/ComfyUI-Crystools/web/*.ts",
+// "../../../web/*.ts",
+// "../../web/*.ts",
+ "web/*.ts",
+// "web/types.d.ts"
],
"exclude": [
"node_modules"
diff --git a/version b/version
index de646d2..3989355 100644
--- a/version
+++ b/version
@@ -1 +1 @@
-1.16.6
+1.20.0
diff --git a/web/comfy/index.d.ts b/web/comfy/index.d.ts
new file mode 100644
index 0000000..8239551
--- /dev/null
+++ b/web/comfy/index.d.ts
@@ -0,0 +1,5 @@
+export type * from './scripts.js';
+export * from './scripts.js';
+export type { ComfyApp } from './typings/comfy.js';
+export * from './liteGraph.js';
+export type * from './liteGraph.js';
diff --git a/web/comfy/index.js b/web/comfy/index.js
new file mode 100644
index 0000000..e364fb0
--- /dev/null
+++ b/web/comfy/index.js
@@ -0,0 +1,2 @@
+export * from './scripts.js';
+export * from './liteGraph.js';
diff --git a/web/comfy/index.ts b/web/comfy/index.ts
new file mode 100644
index 0000000..8239551
--- /dev/null
+++ b/web/comfy/index.ts
@@ -0,0 +1,5 @@
+export type * from './scripts.js';
+export * from './scripts.js';
+export type { ComfyApp } from './typings/comfy.js';
+export * from './liteGraph.js';
+export type * from './liteGraph.js';
diff --git a/web/liteGraph.d.ts b/web/comfy/liteGraph.d.ts
similarity index 74%
rename from web/liteGraph.d.ts
rename to web/comfy/liteGraph.d.ts
index 054996a..52ecb7e 100644
--- a/web/liteGraph.d.ts
+++ b/web/comfy/liteGraph.d.ts
@@ -1,6 +1,5 @@
-export type * from './liteGraph.types';
-export * from '/scripts/widgets.js';
-import type { IWidget as IWidgetOld, LGraphNode as TypeGraphNode, TypeLiteGraph } from './liteGraph.types';
+export type * from './liteGraph.types.js';
+import type { IWidget as IWidgetOld, LGraphNode as TypeGraphNode, TypeLiteGraph } from './liteGraph.types.js';
declare const LGraphNode: typeof TypeGraphNode;
export interface IWidget extends IWidgetOld {
onRemove?: () => void;
@@ -12,9 +11,7 @@ export declare class TLGraphNode extends LGraphNode {
static color: string;
static bgcolor: string;
static collapsable: boolean;
- widgets?: IWidget[];
isVirtualNode?: boolean;
- onResize?: (size: [number, number]) => void;
widgets_values?: any[];
name?: string;
prototype: TLGraphNode;
diff --git a/web/liteGraph.js b/web/comfy/liteGraph.js
similarity index 68%
rename from web/liteGraph.js
rename to web/comfy/liteGraph.js
index fca4a37..38d7b92 100644
--- a/web/liteGraph.js
+++ b/web/comfy/liteGraph.js
@@ -1,25 +1,12 @@
-export * from '/scripts/widgets.js';
export class TLGraphNode extends LGraphNode {
constructor() {
super(...arguments);
- Object.defineProperty(this, "widgets", {
- enumerable: true,
- configurable: true,
- writable: true,
- value: void 0
- });
Object.defineProperty(this, "isVirtualNode", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
- Object.defineProperty(this, "onResize", {
- enumerable: true,
- configurable: true,
- writable: true,
- value: void 0
- });
Object.defineProperty(this, "widgets_values", {
enumerable: true,
configurable: true,
diff --git a/web/liteGraph.ts b/web/comfy/liteGraph.ts
similarity index 79%
rename from web/liteGraph.ts
rename to web/comfy/liteGraph.ts
index 95b8449..e2d4689 100644
--- a/web/liteGraph.ts
+++ b/web/comfy/liteGraph.ts
@@ -1,9 +1,8 @@
// /
// A LOTS OF PATCHES FOR LITEGRAPH TYPES ¯\_(ツ)_/¯
-export type * from './liteGraph.types';
-export * from '/scripts/widgets.js';
+export type * from './liteGraph.types.js';
-import type { IWidget as IWidgetOld, LGraphNode as TypeGraphNode, TypeLiteGraph } from './liteGraph.types';
+import type { IWidget as IWidgetOld, LGraphNode as TypeGraphNode, TypeLiteGraph } from './liteGraph.types.js';
declare const LGraphNode: typeof TypeGraphNode; // just for get the type
@@ -20,9 +19,9 @@ export class TLGraphNode extends LGraphNode {
static bgcolor: string;
static collapsable: boolean;
- widgets?: IWidget[];
+ // widgets?: IWidget[];
isVirtualNode?: boolean;
- onResize?: (size: [number, number]) => void;
+ // override onResize?: (size: [number, number]) => void;
widgets_values?: any[];
name?: string;
diff --git a/web/liteGraph.types.d.ts b/web/comfy/liteGraph.types.d.ts
similarity index 71%
rename from web/liteGraph.types.d.ts
rename to web/comfy/liteGraph.types.d.ts
index bfce629..0281a91 100644
--- a/web/liteGraph.types.d.ts
+++ b/web/comfy/liteGraph.types.d.ts
@@ -1,10 +1,7 @@
// A LOTS OF PATCHES FOR LITEGRAPH TYPES ¯\_(ツ)_/¯
-export type * from '/types/litegraph';
-export type * from '/scripts/widgets';
-export type * from '/scripts/app';
-export type { api } from '/scripts/api';
+import { LGraph } from './typings/litegraph.js';
-import { LGraph, LiteGraph } from '/types/litegraph';
+export type * from './typings/litegraph.js';
export declare type TypeLiteGraph = typeof LiteGraph & {
graph: LGraph;
diff --git a/web/comfy/scripts.d.ts b/web/comfy/scripts.d.ts
new file mode 100644
index 0000000..25a4a60
--- /dev/null
+++ b/web/comfy/scripts.d.ts
@@ -0,0 +1,5 @@
+export { ComfyWidgets } from '../../../scripts/widgets.js';
+export { app } from '../../../scripts/app.js';
+export { api } from '../../../scripts/api.js';
+export * as utils from '../../../scripts/utils.js';
+export { ComfyButtonGroup } from '../../../scripts/ui/components/buttonGroup.js';
diff --git a/web/comfy/scripts.js b/web/comfy/scripts.js
new file mode 100644
index 0000000..25a4a60
--- /dev/null
+++ b/web/comfy/scripts.js
@@ -0,0 +1,5 @@
+export { ComfyWidgets } from '../../../scripts/widgets.js';
+export { app } from '../../../scripts/app.js';
+export { api } from '../../../scripts/api.js';
+export * as utils from '../../../scripts/utils.js';
+export { ComfyButtonGroup } from '../../../scripts/ui/components/buttonGroup.js';
diff --git a/web/comfy/scripts.ts b/web/comfy/scripts.ts
new file mode 100644
index 0000000..6d18b5a
--- /dev/null
+++ b/web/comfy/scripts.ts
@@ -0,0 +1,10 @@
+// @ts-expect-error I could not find a way to make this work
+export { ComfyWidgets } from '../../../scripts/widgets.js';
+// @ts-expect-error I could not find a way to make this work
+export { app } from '../../../scripts/app.js';
+// @ts-expect-error I could not find a way to make this work
+export { api } from '../../../scripts/api.js';
+// @ts-expect-error I could not find a way to make this work
+export * as utils from '../../../scripts/utils.js';
+// @ts-expect-error I could not find a way to make this work
+export { ComfyButtonGroup } from '../../../scripts/ui/components/buttonGroup.js';
diff --git a/web/comfy/typings/README.md b/web/comfy/typings/README.md
new file mode 100644
index 0000000..58fde3e
--- /dev/null
+++ b/web/comfy/typings/README.md
@@ -0,0 +1,5 @@
+COPIED FROM rgthree
+
+The typings in node_modules or in ComfyUI's web/ directory were not that well covered. These typings are hacked together with some of the inconsistencies I found.
+
+To be honest, I have no idea why I needed a bizarre workaround for litegraph's types. Usually the '/// <reference>' comment should have picked up the types, but it wasn't having it. ¯\_(ツ)_/¯
diff --git a/web/comfy/typings/comfy.d.ts b/web/comfy/typings/comfy.d.ts
new file mode 100644
index 0000000..bbecb80
--- /dev/null
+++ b/web/comfy/typings/comfy.d.ts
@@ -0,0 +1,227 @@
+import type { LGraphGroup as TLGraphGroup, LGraphNode as TLGraphNode, IWidget, SerializedLGraphNode, LGraph as TLGraph, LGraphCanvas as TLGraphCanvas, LiteGraph as TLiteGraph } from "./litegraph.js";
+import type {Constructor, SerializedGraph} from './index.js';
+
+declare global {
+ const LiteGraph: typeof TLiteGraph;
+ const LGraph: typeof TLGraph;
+ const LGraphNode: typeof TLGraphNode;
+ const LGraphCanvas: typeof TLGraphCanvas;
+ const LGraphGroup: typeof TLGraphGroup;
+}
+
+// @rgthree: Types on ComfyApp as needed.
+export interface ComfyApp {
+ extensions: ComfyExtension[];
+ async queuePrompt(number?: number, batchCount = 1): Promise;
+ graph: TLGraph;
+ canvas: TLGraphCanvas;
+ clean() : void;
+ registerExtension(extension: ComfyExtension): void;
+ getPreviewFormatParam(): string;
+ getRandParam(): string;
+ loadApiJson(apiData: {}, fileName: string): void;
+ async graphToPrompt(graph?: TLGraph, clean?: boolean): Promise;
+ // workflow: ComfyWorkflowInstance ???
+ async loadGraphData(graphData: {}, clean?: boolean, restore_view?: boolean, workflow?: any|null): Promise
+ ui: {
+ settings: {
+ addSetting(config: {id: string, name: string, type: () => HTMLElement}) : void;
+ }
+ }
+ // Just marking as any for now.
+ menu?: any;
+}
+
+export interface ComfyWidget extends IWidget {
+ // https://github.com/comfyanonymous/ComfyUI/issues/2193 Changes from SerializedLGraphNode to
+ // LGraphNode...
+ serializeValue(nodeType: TLGraphNode, index: number): Promise;
+ afterQueued(): void;
+ inputEl?: HTMLTextAreaElement;
+ width: number;
+}
+
+export interface ComfyGraphNode extends TLGraphNode {
+ getExtraMenuOptions: (node: TLGraphNode, options: ContextMenuItem[]) => void;
+ onExecuted(message: any): void;
+}
+
+export interface ComfyNode extends TLGraphNode {
+ comfyClass: string;
+}
+
+// @rgthree
+export interface ComfyNodeConstructor extends Constructor {
+ static title: string;
+ static type?: string;
+ static comfyClass: string;
+}
+
+export type NodeMode = 0|1|2|3|4|undefined;
+
+
+export interface ComfyExtension {
+ /**
+ * The name of the extension
+ */
+ name: string;
+ /**
+ * Allows any initialisation, e.g. loading resources. Called after the canvas is created but before nodes are added
+ * @param app The ComfyUI app instance
+ */
+ init?(app: ComfyApp): Promise;
+ /**
+ * Allows any additonal setup, called after the application is fully set up and running
+ * @param app The ComfyUI app instance
+ */
+ setup?(app: ComfyApp): Promise;
+ /**
+ * Called before nodes are registered with the graph
+ * @param defs The collection of node definitions, add custom ones or edit existing ones
+ * @param app The ComfyUI app instance
+ */
+ addCustomNodeDefs?(defs: Record, app: ComfyApp): Promise;
+ /**
+ * Allows the extension to add custom widgets
+ * @param app The ComfyUI app instance
+ * @returns An array of {[widget name]: widget data}
+ */
+ getCustomWidgets?(
+ app: ComfyApp
+ ): Promise<
+ Record { widget?: IWidget; minWidth?: number; minHeight?: number }>
+ >;
+ /**
+ * Allows the extension to add additional handling to the node before it is registered with LGraph
+ * @rgthree changed nodeType from `typeof LGraphNode` to `ComfyNodeConstructor`
+ * @param nodeType The node class (not an instance)
+ * @param nodeData The original node object info config object
+ * @param app The ComfyUI app instance
+ */
+ beforeRegisterNodeDef?(nodeType: ComfyNodeConstructor, nodeData: ComfyObjectInfo, app: ComfyApp): Promise;
+ /**
+ * Allows the extension to register additional nodes with LGraph after standard nodes are added
+ * @param app The ComfyUI app instance
+ */
+ // @rgthree - add void for non async
+ registerCustomNodes?(app: ComfyApp): void|Promise;
+ /**
+ * Allows the extension to modify a node that has been reloaded onto the graph.
+ * If you break something in the backend and want to patch workflows in the frontend
+ * This is the place to do this
+ * @param node The node that has been loaded
+ * @param app The ComfyUI app instance
+ */
+ loadedGraphNode?(node: TLGraphNode, app: ComfyApp);
+ /**
+ * Allows the extension to run code after the constructor of the node
+ * @param node The node that has been created
+ * @param app The ComfyUI app instance
+ */
+ nodeCreated?(node: TLGraphNode, app: ComfyApp);
+}
+
+export type ComfyObjectInfo = {
+ name: string;
+ display_name?: string;
+ description?: string;
+ category: string;
+ input?: {
+ required?: Record;
+ optional?: Record;
+ hidden?: Record;
+ };
+ output?: string[];
+ output_name: string[];
+ // @rgthree
+ output_node?: boolean;
+};
+
+export type ComfyObjectInfoConfig = [string | any[]] | [string | any[], any];
+
+// @rgthree
+type ComfyApiInputLink = [
+ /** The id string of the connected node. */
+ string,
+ /** The output index. */
+ number,
+]
+
+// @rgthree
+export type ComfyApiFormatNode = {
+ "inputs": {
+ [input_name: string]: string|number|boolean|ComfyApiInputLink,
+ },
+ "class_type": string,
+ "_meta": {
+ "title": string,
+ }
+}
+
+// @rgthree
+export type ComfyApiFormat = {
+ [node_id: string]: ComfyApiFormatNode
+}
+
+// @rgthree
+export type ComfyApiPrompt = {
+ workflow: SerializedGraph,
+ output: ComfyApiFormat,
+}
+
+// @rgthree
+export type ComfyApiEventDetailStatus = {
+ exec_info: {
+ queue_remaining: number;
+ };
+};
+
+// @rgthree
+export type ComfyApiEventDetailExecutionStart = {
+ prompt_id: string;
+};
+
+// @rgthree
+export type ComfyApiEventDetailExecuting = null | string;
+
+// @rgthree
+export type ComfyApiEventDetailProgress = {
+ node: string;
+ prompt_id: string;
+ max: number;
+ value: number;
+};
+
+// @rgthree
+export type ComfyApiEventDetailExecuted = {
+ node: string;
+ prompt_id: string;
+ output: any;
+};
+
+// @rgthree
+export type ComfyApiEventDetailCached = {
+ nodes: string[];
+ prompt_id: string;
+};
+
+// @rgthree
+export type ComfyApiEventDetailExecuted = {
+ prompt_id: string;
+ node: string;
+ output: any;
+};
+
+// @rgthree
+export type ComfyApiEventDetailError = {
+ prompt_id: string;
+ exception_type: string;
+ exception_message: string;
+ node_id: string;
+ node_type: string;
+ node_id: string;
+ traceback: string;
+ executed: any[];
+ current_inputs: {[key: string]: (number[]|string[])};
+ current_outputs: {[key: string]: (number[]|string[])};
+}
diff --git a/web/comfy/typings/index.d.ts b/web/comfy/typings/index.d.ts
new file mode 100644
index 0000000..a574726
--- /dev/null
+++ b/web/comfy/typings/index.d.ts
@@ -0,0 +1,55 @@
+import { LGraph } from "./litegraph.js";
+
+export type Constructor = new(...args: any[]) => T;
+
+export type SerializedLink = [
+ number, // this.id,
+ number, // this.origin_id,
+ number, // this.origin_slot,
+ number, // this.target_id,
+ number, // this.target_slot,
+ string, // this.type
+];
+
+export interface SerializedNodeInput {
+ name: string;
+ type: string;
+ link: number;
+}
+export interface SerializedNodeOutput {
+ name: string;
+ type: string;
+ link: number;
+ slot_index: number;
+ links: number[];
+}
+export interface SerializedNode {
+ id: number;
+ inputs: SerializedNodeInput[];
+ outputs: SerializedNodeOutput[];
+ mode: number;
+ order: number;
+ pos: [number, number];
+ properties: any;
+ size: [number, number];
+ type: string;
+ widgets_values: Array;
+}
+
+export interface SerializedGraph {
+ config: any;
+ extra: any;
+ groups: any;
+ last_link_id: number;
+ last_node_id: number;
+ links: SerializedLink[];
+ nodes: SerializedNode[];
+}
+
+export interface BadLinksData {
+ hasBadLinks: boolean;
+ fixed: boolean;
+ graph: T;
+ patched: number;
+ deleted: number;
+}
diff --git a/web/comfy/typings/litegraph.d.ts b/web/comfy/typings/litegraph.d.ts
new file mode 100644
index 0000000..730103e
--- /dev/null
+++ b/web/comfy/typings/litegraph.d.ts
@@ -0,0 +1,1750 @@
+// Type definitions for litegraph.js 0.7.0
+// Project: litegraph.js
+// Definitions by: NateScarlet
+
+export type Vector2 = [number, number];
+export type Vector4 = [number, number, number, number];
+export type widgetTypes =
+ | "number"
+ | "slider"
+ | "combo"
+ | "text"
+ | "toggle"
+ | "button";
+export type SlotShape =
+ | typeof LiteGraph.BOX_SHAPE
+ | typeof LiteGraph.CIRCLE_SHAPE
+ | typeof LiteGraph.ARROW_SHAPE
+ | typeof LiteGraph.SQUARE_SHAPE
+ | number; // For custom shapes
+
+/** https://github.com/jagenjo/litegraph.js/tree/master/guides#node-slots */
+export interface INodeSlot {
+ name: string;
+ type: string | -1;
+ label?: string;
+ dir?:
+ | typeof LiteGraph.UP
+ | typeof LiteGraph.RIGHT
+ | typeof LiteGraph.DOWN
+ | typeof LiteGraph.LEFT;
+ color_on?: string;
+ color_off?: string;
+ shape?: SlotShape;
+ locked?: boolean;
+ nameLocked?: boolean;
+ pos?: Vector2;
+ // @rgthree
+ hidden?: boolean;
+ // @rgthree
+ disabled?: boolean;
+ // @rgthree - Found this checked in getSlotMenuOptions default.
+ removable?: boolean;
+ // @rgthree - A status we put on some nodes so we can draw things around it.
+ rgthree_status?: 'WARN' | 'ERROR';
+}
+
+export interface INodeInputSlot extends INodeSlot {
+ link: LLink["id"] | null;
+ // @rgthree - add comfy widget info
+ widget?: {
+ name: string;
+ }
+}
+
+export interface INodeOutputSlot extends INodeSlot {
+ links: LLink["id"][] | null;
+}
+
+export type WidgetCallback = (
+ this: T,
+ value: T["value"],
+ graphCanvas: LGraphCanvas,
+ node: LGraphNode,
+ pos: Vector2,
+ event?: MouseEvent
+) => void;
+
+// @rgthree
+export type WidgetComboCallback = (
+ this: T,
+ value: T["value"][0],
+ graphCanvas: LGraphCanvas,
+ node: LGraphNode,
+ pos: Vector2,
+ event?: MouseEvent
+) => void;
+
+// @rgthree
+export type IWidgetOptions = {
+ y?: number; // ?
+ property?: string;
+ serialize?: boolean; // ComfyUI in app.js
+ forceInput?: boolean; // ComfyUI in app.js
+ defaultInput?: boolean; // ComfyUI in app.js
+}
+
+// @rgthree
+export type IWidgetToggleOptions = IWidgetOptions & {
+ on?: string;
+ off?: string;
+}
+
+// @rgthree
+export type IWidgetNumberOptions = IWidgetOptions & {
+ precision?: number;
+ max?: number;
+ min?: number;
+}
+
+// @rgthree
+export type IWidgetSliderOptions = IWidgetNumberOptions & {
+ slider_color?: string;
+ marker_color?: string;
+}
+
+
+// @rgthree
+export type IWidgetComboOptions = IWidgetOptions & {
+ values?: string[] | ((widget: IComboWidget, node: LGraphNode) => string[]);
+}
+
+export interface IWidget {
+ name: string | null;
+ // @rgthree
+ label?: string | null;
+ value: TValue;
+ options?: TOptions;
+ // @rgthree - extend to string for custom
+ type?: widgetTypes | string;
+ y?: number;
+ property?: string;
+ last_y?: number;
+ clicked?: boolean;
+ marker?: boolean;
+ disabled?: boolean;
+ callback?: WidgetCallback;
+ // crystools
+ onRemove?: () => void;
+ /** Called by `LGraphCanvas.drawNodeWidgets` */
+ draw?(
+ ctx: CanvasRenderingContext2D,
+ node: LGraphNode,
+ width: number,
+ posY: number,
+ height: number
+ ): void;
+ /**
+ * Called by `LGraphCanvas.processNodeWidgets`
+ * https://github.com/jagenjo/litegraph.js/issues/76
+ */
+ mouse?(
+ event: MouseEvent,
+ pos: Vector2,
+ node: LGraphNode
+ ): boolean;
+ /** Called by `LGraphNode.computeSize` */
+ computeSize?(width: number): [number, number];
+ // @rgthree - make optional, since it is in the code.
+ serializeValue?(serializedNode: SerializedLGraphNode, widgetIndex: number): TValue;
+ // @rgthree - Checked in LGraphCanvas.prototype.processNodeWidgets, and figured I'd use it too.
+ width?: number;
+}
+export interface IButtonWidget extends IWidget {
+ type: "button";
+}
+// @rgthree: adding options
+export interface IToggleWidget extends IWidget {
+ type: "toggle";
+}
+// @rgthree: adding options
+export interface ISliderWidget extends IWidget {
+ type: "slider";
+}
+// @rgthree: adding options
+export interface INumberWidget extends IWidget {
+ type: "number";
+}
+// @rgthree: adding options
+export interface IComboWidget extends IWidget {
+ value: T[0];
+ type: "combo";
+ callback?: WidgetComboCallback;
+}
+
+export interface ITextWidget extends IWidget {
+ type: "text";
+}
+
+export interface IContextMenuItem {
+ // @rgthree - Make optional because, I guess it is?
+ content?: string;
+ value?: any;
+ callback?: ContextMenuEventListener;
+ /** Used as innerHTML for extra child element */
+ title?: string;
+ disabled?: boolean;
+ has_submenu?: boolean;
+ submenu?: {
+ options: ContextMenuItem[];
+ } & IContextMenuOptions;
+ className?: string;
+ // @rgthree - Added for menu_auto_nest
+ rgthree_originalValue?: IContextMenuItem;
+ // @rgthree - this was missing and passed through for getSlotMenuOptions default.
+ slot?: {input?: INodeInputSlot, output?: INodeOutputSlot};
+}
+export interface IContextMenuOptions {
+ callback?: ContextMenuEventListener;
+ ignore_item_callbacks?: Boolean;
+ event?: MouseEvent | CustomEvent | AdjustedMouseEvent;
+ parentMenu?: ContextMenu|null;
+ autoopen?: boolean;
+ title?: string;
+ extra?: any;
+ // @rgthree
+ scale?: number;
+ // @rgthree
+ left?: number;
+ // @rgthree
+ top?: number;
+ // @rgthree
+ className?: string;
+ // @rgthree - Added for menu_auto_nest
+ rgthree_originalCallback?: ContextMenuEventListener;
+ // @rgthree - No idea since it's not documented, but we'll use it to pass data, like rgthree_doNotNest
+ extra?: any
+}
+
+export type ContextMenuItem = IContextMenuItem | null;
+export type ContextMenuEventListener = (
+ value: ContextMenuItem,
+ options: IContextMenuOptions,
+ event: MouseEvent,
+ parentMenu: ContextMenu | undefined,
+ node: LGraphNode
+) => boolean | void;
+
+export const LiteGraph: {
+ VERSION: number;
+
+ CANVAS_GRID_SIZE: number;
+
+ NODE_TITLE_HEIGHT: number;
+ NODE_TITLE_TEXT_Y: number;
+ NODE_SLOT_HEIGHT: number;
+ NODE_WIDGET_HEIGHT: number;
+ NODE_WIDTH: number;
+ NODE_MIN_WIDTH: number;
+ NODE_COLLAPSED_RADIUS: number;
+ NODE_COLLAPSED_WIDTH: number;
+ NODE_TITLE_COLOR: string;
+ NODE_TEXT_SIZE: number;
+ NODE_TEXT_COLOR: string;
+ NODE_SUBTEXT_SIZE: number;
+ NODE_DEFAULT_COLOR: string;
+ NODE_DEFAULT_BGCOLOR: string;
+ NODE_DEFAULT_BOXCOLOR: string;
+ NODE_DEFAULT_SHAPE: string;
+ // @rgthree - didn't exist.
+ NODE_BOX_OUTLINE_COLOR: string;
+ DEFAULT_SHADOW_COLOR: string;
+ DEFAULT_GROUP_FONT: number;
+ // @rgthree - Seems to have been missing.
+ NODE_BOX_OUTLINE_COLOR: string;
+
+ WIDGET_BGCOLOR: string;
+ WIDGET_OUTLINE_COLOR: string;
+ WIDGET_TEXT_COLOR: string;
+ WIDGET_SECONDARY_TEXT_COLOR: string;
+
+ LINK_COLOR: string;
+ EVENT_LINK_COLOR: string;
+ CONNECTING_LINK_COLOR: string;
+
+ MAX_NUMBER_OF_NODES: number; //avoid infinite loops
+ DEFAULT_POSITION: Vector2; //default node position
+ VALID_SHAPES: ["default", "box", "round", "card"]; //,"circle"
+
+ //shapes are used for nodes but also for slots
+ BOX_SHAPE: 1;
+ ROUND_SHAPE: 2;
+ CIRCLE_SHAPE: 3;
+ CARD_SHAPE: 4;
+ ARROW_SHAPE: 5;
+ GRID_SHAPE: 6;
+
+ //enums
+ INPUT: 1;
+ OUTPUT: 2;
+
+ EVENT: -1; //for outputs
+ ACTION: -1; //for inputs
+
+ ALWAYS: 0;
+ ON_EVENT: 1;
+ NEVER: 2;
+ ON_TRIGGER: 3;
+
+ UP: 1;
+ DOWN: 2;
+ LEFT: 3;
+ RIGHT: 4;
+ CENTER: 5;
+
+ STRAIGHT_LINK: 0;
+ LINEAR_LINK: 1;
+ SPLINE_LINK: 2;
+
+ NORMAL_TITLE: 0;
+ NO_TITLE: 1;
+ TRANSPARENT_TITLE: 2;
+ AUTOHIDE_TITLE: 3;
+
+ node_images_path: string;
+
+ // @rgthree. These just weren't there. Note, LiteGraph initializes these as an array, but
+ // ComfyUI overrides these to a string-keye'd object... ???
+ slot_types_default_out: {[key: string]: string[]};
+ slot_types_default_in: {[key: string]: string[]};
+
+ debug: boolean;
+ catch_exceptions: boolean;
+ throw_errors: boolean;
+ /** if set to true some nodes like Formula would be allowed to evaluate code that comes from unsafe sources (like node configuration), which could lead to exploits */
+ allow_scripts: boolean;
+ /** node types by string */
+ registered_node_types: Record;
+ /** used for dropping files in the canvas */
+ node_types_by_file_extension: Record;
+ /** node types by class name */
+ Nodes: Record;
+
+ /** used to add extra features to the search box */
+ searchbox_extras: Record<
+ string,
+ {
+ data: { outputs: string[][]; title: string };
+ desc: string;
+ type: string;
+ }
+ >;
+
+ //@rgthree
+ isValidConnection(type: string|string[], type: string|string[]):boolean;
+ overlapBounding(a: Vector4, b: Vector4) : boolean;
+
+ createNode(type: string): T;
+ /** Register a node class so it can be listed when the user wants to create a new one */
+ registerNodeType(type: string, base: { new (title?: string): T }): void;
+ /** removes a node type from the system */
+ unregisterNodeType(type: string): void;
+ /** Removes all previously registered node's types. */
+ clearRegisteredTypes(): void;
+ /**
+ * Create a new node type by passing a function, it wraps it with a proper class and generates inputs according to the parameters of the function.
+ * Useful to wrap simple methods that do not require properties, and that only process some input to generate an output.
+ * @param name node name with namespace (p.e.: 'math/sum')
+ * @param func
+ * @param param_types an array containing the type of every parameter, otherwise parameters will accept any type
+ * @param return_type string with the return type, otherwise it will be generic
+ * @param properties properties to be configurable
+ */
+ wrapFunctionAsNode(
+ name: string,
+ func: (...args: any[]) => any,
+ param_types?: string[],
+ return_type?: string,
+ properties?: object
+ ): void;
+
+ /**
+ * Adds this method to all node types, existing and to be created
+ * (You can add it to LGraphNode.prototype but then existing node types wont have it)
+ */
+ addNodeMethod(name: string, func: (...args: any[]) => any): void;
+
+ /**
+ * Create a node of a given type with a name. The node is not attached to any graph yet.
+ * @param type full name of the node class. p.e. "math/sin"
+ * @param name a name to distinguish from other nodes
+ * @param options to set options
+ */
+ createNode(
+ type: string,
+ title: string,
+ options: object
+ ): T;
+
+ /**
+ * Returns a registered node type with a given name
+ * @param type full name of the node class. p.e. "math/sin"
+ */
+ getNodeType(type: string): LGraphNodeConstructor;
+
+ /**
+ * Returns a list of node types matching one category
+ * @method getNodeTypesInCategory
+ * @param {String} category category name
+ * @param {String} filter only nodes with ctor.filter equal can be shown
+ * @return {Array} array with all the node classes
+ */
+ getNodeTypesInCategory(
+ category: string,
+ filter: string
+ ): LGraphNodeConstructor[];
+
+ /**
+ * Returns a list with all the node type categories
+ * @method getNodeTypesCategories
+ * @param {String} filter only nodes with ctor.filter equal can be shown
+ * @return {Array} array with all the names of the categories
+ */
+ getNodeTypesCategories(filter: string): string[];
+
+ /** debug purposes: reloads all the js scripts that matches a wildcard */
+ reloadNodes(folder_wildcard: string): void;
+
+ getTime(): number;
+ LLink: typeof LLink;
+ LGraph: typeof LGraph;
+ DragAndScale: typeof DragAndScale;
+ compareObjects(a: object, b: object): boolean;
+ distance(a: Vector2, b: Vector2): number;
+ colorToString(c: string): string;
+ isInsideRectangle(
+ x: number,
+ y: number,
+ left: number,
+ top: number,
+ width: number,
+ height: number
+ ): boolean;
+ growBounding(bounding: Vector4, x: number, y: number): Vector4;
+ isInsideBounding(p: Vector2, bb: Vector4): boolean;
+ hex2num(hex: string): [number, number, number];
+ num2hex(triplet: [number, number, number]): string;
+ ContextMenu: typeof ContextMenu;
+ extendClass(target: A, origin: B): A & B;
+ getParameterNames(func: string): string[];
+ // @rgthree
+ closeAllContextMenus(ref_window?: Window): void;
+};
+
+export type serializedLGraph<
+ TNode = ReturnType,
+ // https://github.com/jagenjo/litegraph.js/issues/74
+ TLink = [number, number, number, number, number, string],
+ TGroup = ReturnType
+> = {
+ last_node_id: LGraph["last_node_id"];
+ last_link_id: LGraph["last_link_id"];
+ nodes: TNode[];
+ links: TLink[];
+ groups: TGroup[];
+ config: LGraph["config"];
+ version: typeof LiteGraph.VERSION;
+};
+
+export declare class LGraph {
+ static supported_types: string[];
+ static STATUS_STOPPED: 1;
+ static STATUS_RUNNING: 2;
+
+ constructor(o?: object);
+
+ filter: string;
+ catch_errors: boolean;
+ /** custom data */
+ config: object;
+ elapsed_time: number;
+ fixedtime: number;
+ fixedtime_lapse: number;
+ globaltime: number;
+ inputs: any;
+ iteration: number;
+ last_link_id: number;
+ last_node_id: number;
+ last_update_time: number;
+ links: Record;
+ list_of_graphcanvas: LGraphCanvas[];
+ outputs: any;
+ runningtime: number;
+ starttime: number;
+ status: typeof LGraph.STATUS_RUNNING | typeof LGraph.STATUS_STOPPED;
+
+ // @rgthree, remove private; it's not really private b/c it's javascript.
+ _nodes: LGraphNode[];
+ // @rgthree, remove private; it's not really private b/c it's javascript.
+ _groups: LGraphGroup[];
+ private _nodes_by_id: Record;
+ /** nodes that are executable sorted in execution order */
+ private _nodes_executable:
+ | (LGraphNode & { onExecute: NonNullable }[])
+ | null;
+ /** nodes that contain onExecute */
+ private _nodes_in_order: LGraphNode[];
+ private _version: number;
+
+ getSupportedTypes(): string[];
+ /** Removes all nodes from this graph */
+ clear(): void;
+ /** Attach Canvas to this graph */
+ attachCanvas(graphCanvas: LGraphCanvas): void;
+ /** Detach Canvas to this graph */
+ detachCanvas(graphCanvas: LGraphCanvas): void;
+ /**
+ * Starts running this graph every interval milliseconds.
+ * @param interval amount of milliseconds between executions, if 0 then it renders to the monitor refresh rate
+ */
+ start(interval?: number): void;
+ /** Stops the execution loop of the graph */
+ stop(): void;
+ /**
+ * Run N steps (cycles) of the graph
+ * @param num number of steps to run, default is 1
+ */
+ runStep(num?: number, do_not_catch_errors?: boolean): void;
+ /**
+ * Updates the graph execution order according to relevance of the nodes (nodes with only outputs have more relevance than
+ * nodes with only inputs.
+ */
+ updateExecutionOrder(): void;
+ /** This is more internal, it computes the executable nodes in order and returns it */
+ computeExecutionOrder(only_onExecute: boolean, set_level: any): T;
+ /**
+ * Returns all the nodes that could affect this one (ancestors) by crawling all the inputs recursively.
+ * It doesn't include the node itself
+ * @return an array with all the LGraphNodes that affect this node, in order of execution
+ */
+ getAncestors(node: LGraphNode): LGraphNode[];
+ /**
+ * Positions every node in a more readable manner
+ */
+ arrange(margin?: number,layout?: string): void;
+ /**
+ * Returns the amount of time the graph has been running in milliseconds
+ * @return number of milliseconds the graph has been running
+ */
+ getTime(): number;
+
+ /**
+ * Returns the amount of time accumulated using the fixedtime_lapse var. This is used in context where the time increments should be constant
+ * @return number of milliseconds the graph has been running
+ */
+ getFixedTime(): number;
+
+ /**
+ * Returns the amount of time it took to compute the latest iteration. Take into account that this number could be not correct
+ * if the nodes are using graphical actions
+ * @return number of milliseconds it took the last cycle
+ */
+ getElapsedTime(): number;
+ /**
+ * Sends an event to all the nodes, useful to trigger stuff
+ * @param eventName the name of the event (function to be called)
+ * @param params parameters in array format
+ */
+ sendEventToAllNodes(eventName: string, params: any[], mode?: any): void;
+
+ sendActionToCanvas(action: any, params: any[]): void;
+ /**
+ * Adds a new node instance to this graph
+ * @param node the instance of the node
+ */
+ add(node: LGraphNode, skip_compute_order?: boolean): void;
+ /**
+ * Called when a new node is added
+ * @param node the instance of the node
+ */
+ onNodeAdded(node: LGraphNode): void;
+ /** Removes a node from the graph */
+ remove(node: LGraphNode): void;
+ /**
+ * Returns a node by its id.
+ * @rgthree - make id options/nullable.
+ */
+ getNodeById(id?: number|null): LGraphNode | undefined;
+ /**
+ * Returns a list of nodes that matches a class
+ * @param classObject the class itself (not an string)
+ * @return a list with all the nodes of this type
+ */
+ findNodesByClass(
+ classObject: LGraphNodeConstructor
+ ): T[];
+ /**
+ * Returns a list of nodes that matches a type
+ * @param type the name of the node type
+ * @return a list with all the nodes of this type
+ */
+ findNodesByType(type: string): T[];
+ /**
+ * Returns the first node that matches a name in its title
+ * @param title the name of the node to search
+ * @return the node or null
+ */
+ findNodeByTitle(title: string): T | null;
+ /**
+ * Returns a list of nodes that matches a name
+ * @param title the name of the node to search
+ * @return a list with all the nodes with this name
+ */
+ findNodesByTitle(title: string): T[];
+ /**
+ * Returns the top-most node in this position of the canvas
+ * @param x the x coordinate in canvas space
+ * @param y the y coordinate in canvas space
+ * @param nodes_list a list with all the nodes to search from, by default is all the nodes in the graph
+ * @return the node at this position or null
+ */
+ getNodeOnPos(
+ x: number,
+ y: number,
+ node_list?: LGraphNode[],
+ margin?: number
+ ): T | null;
+ /**
+ * Returns the top-most group in that position
+ * @param x the x coordinate in canvas space
+ * @param y the y coordinate in canvas space
+ * @return the group or null
+ */
+ getGroupOnPos(x: number, y: number): LGraphGroup | null;
+
+ onAction(action: any, param: any): void;
+ trigger(action: any, param: any): void;
+ /** Tell this graph it has a global graph input of this type */
+ addInput(name: string, type: string, value?: any): void;
+ /** Assign a data to the global graph input */
+ setInputData(name: string, data: any): void;
+ /** Returns the current value of a global graph input */
+ getInputData(name: string): T;
+ /** Changes the name of a global graph input */
+ renameInput(old_name: string, name: string): false | undefined;
+ /** Changes the type of a global graph input */
+ changeInputType(name: string, type: string): false | undefined;
+ /** Removes a global graph input */
+ removeInput(name: string): boolean;
+ /** Creates a global graph output */
+ addOutput(name: string, type: string, value: any): void;
+ /** Assign a data to the global output */
+ setOutputData(name: string, value: string): void;
+ /** Returns the current value of a global graph output */
+ getOutputData(name: string): T;
+
+ /** Renames a global graph output */
+ renameOutput(old_name: string, name: string): false | undefined;
+ /** Changes the type of a global graph output */
+ changeOutputType(name: string, type: string): false | undefined;
+ /** Removes a global graph output */
+ removeOutput(name: string): boolean;
+ triggerInput(name: string, value: any): void;
+ setCallback(name: string, func: (...args: any[]) => any): void;
+ beforeChange(info?: LGraphNode): void;
+ afterChange(info?: LGraphNode): void;
+ connectionChange(node: LGraphNode): void;
+ /** returns if the graph is in live mode */
+ isLive(): boolean;
+ /** clears the triggered slot animation in all links (stop visual animation) */
+ clearTriggeredSlots(): void;
+ /* Called when something visually changed (not the graph!) */
+ change(): void;
+ setDirtyCanvas(fg: boolean, bg: boolean): void;
+ /** Destroys a link */
+ removeLink(link_id: number): void;
+ /** Creates a Object containing all the info about this graph, it can be serialized */
+ serialize(): T;
+ /**
+ * Configure a graph from a JSON string
+ * @param data configure a graph from a JSON string
+ * @returns if there was any error parsing
+ */
+ configure(data: object, keep_old?: boolean): boolean | undefined;
+ load(url: string): void;
+}
+
+export type SerializedLLink = [number, string, number, number, number, number];
+export declare class LLink {
+ id: number;
+ type: string;
+ origin_id: number;
+ origin_slot: number;
+ target_id: number;
+ target_slot: number;
+ constructor(
+ id: number,
+ type: string,
+ origin_id: number,
+ origin_slot: number,
+ target_id: number,
+ target_slot: number
+ );
+ configure(o: LLink | SerializedLLink): void;
+ serialize(): SerializedLLink;
+ // @rgthree
+ color?: string;
+}
+
+export type SerializedLGraphNode = {
+ id: T["id"];
+ type: T["type"];
+ pos: T["pos"];
+ size: T["size"];
+ flags: T["flags"];
+ mode: T["mode"];
+ inputs: T["inputs"];
+ outputs: T["outputs"];
+ title: T["title"];
+ properties: T["properties"];
+ widgets_values?: IWidget["value"][];
+};
+
+/** https://github.com/jagenjo/litegraph.js/blob/master/guides/README.md#lgraphnode */
+export declare class LGraphNode {
+
+ // @rgthree added
+ findInputSlotByType(type: string, returnObj?: boolean, preferFreeSlot?: boolean, doNotUseOccupied?: boolean): number
+ findOutputSlotByType(type: string, returnObj?: boolean, preferFreeSlot?: boolean, doNotUseOccupied?: boolean): number
+ onShowCustomPanelInfo(panel: HTMLElement): void;
+ onDblClick?(event: AdjustedMouseEvent, pos: Vector2, canvas: LGraphCanvas): void;
+ inResizeCorner(x: number, y:number) : boolean;
+ onWidgetChanged?(widgetName: string, widgetValue: any, oldWidgetValue: any, widget: IWidget): void;
+
+ // end @rgthree added
+
+ static title_color?: string;
+ static title: string;
+ static type: null | string;
+ static widgets_up: boolean;
+ constructor(title?: string);
+
+ title: string;
+ // @rgthree - made undefined since ComfyNode does it (even through LiteGraph does not..)
+ type?: null | string;
+ size: Vector2;
+ graph: null | LGraph;
+ graph_version: number;
+ pos: Vector2;
+ is_selected: boolean;
+ mouseOver: boolean;
+ // @rgthree - missing.
+ block_delete: boolean;
+
+ id: number;
+
+ widgets: IWidget[];
+ //inputs available: array of inputs
+ inputs: INodeInputSlot[];
+ outputs: INodeOutputSlot[];
+ connections: any[];
+
+ // @rgthree
+ _collapsed_width?: number;
+
+ //local data
+ properties: Record;
+ properties_info: any[];
+
+ flags: Partial<{
+ collapsed: boolean
+ // @rgthree
+ allow_interaction: boolean;
+ // @rgthree
+ pinned: boolean;
+ }>;
+
+ color: string;
+ bgcolor: string;
+ boxcolor: string;
+ shape:
+ | typeof LiteGraph.BOX_SHAPE
+ | typeof LiteGraph.ROUND_SHAPE
+ | typeof LiteGraph.CIRCLE_SHAPE
+ | typeof LiteGraph.CARD_SHAPE
+ | typeof LiteGraph.ARROW_SHAPE;
+
+ serialize_widgets: boolean;
+ skip_list: boolean;
+
+ /** Used in `LGraphCanvas.onMenuNodeMode` */
+ mode?:
+ | typeof LiteGraph.ON_EVENT
+ | typeof LiteGraph.ON_TRIGGER
+ | typeof LiteGraph.NEVER
+ | typeof LiteGraph.ALWAYS
+ | 4; // Comfy App "Bypass"
+
+ /** If set to true widgets do not start after the slots */
+ widgets_up: boolean;
+ /** widgets start at y distance from the top of the node */
+ widgets_start_y: number;
+ /** if you render outside the node, it will be clipped */
+ clip_area: boolean;
+ /** if set to false it wont be resizable with the mouse */
+ resizable: boolean;
+ /** slots are distributed horizontally */
+ horizontal: boolean;
+ /** if true, the node will show the bgcolor as 'red' */
+ has_errors?: boolean;
+
+ // @rgthree
+ setSize(size: Vector2): void;
+ onResize?(size: Vector2): void;
+ onInputClick(slot: number, event: MouseEvent): void;
+ onOutputClick(slot: number, event: MouseEvent): void;
+ getConnectionPos(isInput: boolean, slotNumber: number, out: Vector2): Vector2;
+
+ /** configure a node from an object containing the serialized info */
+ configure(info: SerializedLGraphNode): void;
+ /** serialize the content */
+ serialize(): SerializedLGraphNode;
+ /** Creates a clone of this node */
+ clone(): this;
+ /** serialize and stringify */
+ toString(): string;
+ /** get the title string */
+ getTitle(): string;
+ /** sets the value of a property */
+ setProperty(name: string, value: any): void;
+ /** sets the output data */
+ setOutputData(slot: number, data: any): void;
+ /** sets the output data */
+ setOutputDataType(slot: number, type: string): void;
+ /**
+ * Retrieves the input data (data traveling through the connection) from one slot
+ * @param slot
+ * @param force_update if set to true it will force the connected node of this slot to output data into this link
+ * @return data or if it is not connected returns undefined
+ */
+ getInputData(slot: number, force_update?: boolean): T;
+ /**
+ * Retrieves the input data type (in case this supports multiple input types)
+ * @param slot
+ * @return datatype in string format
+ */
+ getInputDataType(slot: number): string;
+ /**
+ * Retrieves the input data from one slot using its name instead of slot number
+ * @param slot_name
+ * @param force_update if set to true it will force the connected node of this slot to output data into this link
+ * @return data or if it is not connected returns null
+ */
+ getInputDataByName(slot_name: string, force_update?: boolean): T;
+ /** tells you if there is a connection in one input slot */
+ isInputConnected(slot: number): boolean;
+ /** tells you info about an input connection (which node, type, etc) */
+ getInputInfo(
+ slot: number
+ ): { link: number; name: string; type: string | 0 } | null;
+ /** returns the node connected in the input slot */
+ getInputNode(slot: number): LGraphNode | null;
+ /** returns the value of an input with this name, otherwise checks if there is a property with that name */
+ getInputOrProperty(name: string): T;
+ /** tells you the last output data that went in that slot */
+ getOutputData(slot: number): T | null;
+ /** tells you info about an output connection (which node, type, etc) */
+ getOutputInfo(
+ slot: number
+ ): { name: string; type: string; links: number[] } | null;
+ /** tells you if there is a connection in one output slot */
+ isOutputConnected(slot: number): boolean;
+ /** tells you if there is any connection in the output slots */
+ isAnyOutputConnected(): boolean;
+ /** retrieves all the nodes connected to this output slot */
+ getOutputNodes(slot: number): LGraphNode[];
+ /** Triggers an event in this node, this will trigger any output with the same name */
+ trigger(action: string, param: any): void;
+ /**
+ * Triggers an slot event in this node
+ * @param slot the index of the output slot
+ * @param param
+ * @param link_id in case you want to trigger and specific output link in a slot
+ */
+ triggerSlot(slot: number, param: any, link_id?: number): void;
+ /**
+ * clears the trigger slot animation
+ * @param slot the index of the output slot
+ * @param link_id in case you want to trigger and specific output link in a slot
+ */
+ clearTriggeredSlot(slot: number, link_id?: number): void;
+ /**
+ * add a new property to this node
+ * @param name
+ * @param default_value
+ * @param type string defining the output type ("vec3","number",...)
+ * @param extra_info this can be used to have special properties of the property (like values, etc)
+ */
+ addProperty(
+ name: string,
+ default_value: any,
+ type: string,
+ extra_info?: object
+ ): T;
+ /**
+ * add a new output slot to use in this node
+ * @param name
+ * @param type string defining the output type ("vec3","number",...)
+ * @param extra_info this can be used to have special properties of an output (label, special color, position, etc)
+ */
+ addOutput(
+ name: string,
+ type: string | -1,
+ extra_info?: Partial
+ ): INodeOutputSlot;
+ /**
+ * add a new output slot to use in this node
+ * @param array of triplets like [[name,type,extra_info],[...]]
+ */
+ addOutputs(
+ array: [string, string | -1, Partial | undefined][]
+ ): void;
+ /** remove an existing output slot */
+ removeOutput(slot: number): void;
+ /**
+ * add a new input slot to use in this node
+ * @param name
+ * @param type string defining the input type ("vec3","number",...), it its a generic one use 0
+ * @param extra_info this can be used to have special properties of an input (label, color, position, etc)
+ */
+ addInput(
+ name: string,
+ type: string | -1,
+ extra_info?: Partial
+ ): INodeInputSlot;
+ /**
+ * add several new input slots in this node
+ * @param array of triplets like [[name,type,extra_info],[...]]
+ */
+ addInputs(
+ array: [string, string | -1, Partial | undefined][]
+ ): void;
+ /** remove an existing input slot */
+ removeInput(slot: number): void;
+ /**
+ * add an special connection to this node (used for special kinds of graphs)
+ * @param name
+ * @param type string defining the input type ("vec3","number",...)
+ * @param pos position of the connection inside the node
+ * @param direction if is input or output
+ */
+ addConnection(
+ name: string,
+ type: string,
+ pos: Vector2,
+ direction: string
+ ): {
+ name: string;
+ type: string;
+ pos: Vector2;
+ direction: string;
+ links: null;
+ };
+ setValue(v: any): void;
+ /** computes the size of a node according to its inputs and output slots */
+ computeSize(out?: Vector2): [number, number];
+ /**
+ * https://github.com/jagenjo/litegraph.js/blob/master/guides/README.md#node-widgets
+ * @return created widget
+ */
+ addWidget(
+ type: T["type"],
+ name: string,
+ value: T["value"],
+ // @rgthree
+ callback?: T["callback"] | string,//WidgetCallback | string,
+ options?: T["options"]
+ ): T;
+
+ addCustomWidget(customWidget: T): T;
+
+ /**
+ * returns the bounding of the object, used for rendering purposes
+ * @return [x, y, width, height]
+ */
+ getBounding(): Vector4;
+ /** checks if a point is inside the shape of a node */
+ isPointInside(
+ x: number,
+ y: number,
+ margin?: number,
+ skipTitle?: boolean
+ ): boolean;
+ /** checks if a point is inside a node slot, and returns info about which slot */
+ getSlotInPosition(
+ x: number,
+ y: number
+ ): {
+ input?: INodeInputSlot;
+ output?: INodeOutputSlot;
+ slot: number;
+ link_pos: Vector2;
+ };
+ /**
+ * returns the input slot with a given name (used for dynamic slots), -1 if not found
+ * @param name the name of the slot
+ * @return the slot (-1 if not found)
+ */
+ findInputSlot(name: string): number;
+ /**
+ * returns the output slot with a given name (used for dynamic slots), -1 if not found
+ * @param name the name of the slot
+ * @return the slot (-1 if not found)
+ */
+ findOutputSlot(name: string): number;
+ /**
+ * connect this node output to the input of another node
+ * @param slot (could be the number of the slot or the string with the name of the slot)
+ * @param targetNode the target node
+ * @param targetSlot the input slot of the target node (could be the number of the slot or the string with the name of the slot, or -1 to connect a trigger)
+ * @return {Object} the link_info is created, otherwise null
+ */
+ connect(
+ slot: number | string,
+ targetNode: LGraphNode,
+ targetSlot: number | string
+ ): T | null;
+
+ connectByTypeOutput(
+ slot: number | string,
+ sourceNode: LGraphNode,
+ sourceSlotType: string,
+ optsIn: string
+ ): T | null;
+
+ connectByType(
+ slot: number | string,
+ sourceNode: LGraphNode,
+ sourceSlotType: string,
+ optsIn: string
+ ): T | null;
+
+
+ /**
+ * disconnect one output to an specific node
+ * @param slot (could be the number of the slot or the string with the name of the slot)
+ * @param target_node the target node to which this slot is connected [Optional, if not target_node is specified all nodes will be disconnected]
+ * @return if it was disconnected successfully
+ */
+ disconnectOutput(slot: number | string, targetNode?: LGraphNode): boolean;
+ /**
+ * disconnect one input
+ * @param slot (could be the number of the slot or the string with the name of the slot)
+ * @return if it was disconnected successfully
+ */
+ disconnectInput(slot: number | string): boolean;
+ /**
+ * returns the center of a connection point in canvas coords
+ * @param is_input true if if a input slot, false if it is an output
+ * @param slot (could be the number of the slot or the string with the name of the slot)
+ * @param out a place to store the output, to free garbage
+ * @return the position
+ **/
+ getConnectionPos(
+ is_input: boolean,
+ slot: number | string,
+ out?: Vector2
+ ): Vector2;
+ /** Force align to grid */
+ alignToGrid(): void;
+ /** Console output */
+ trace(msg: string): void;
+ /** Forces to redraw or the main canvas (LGraphNode) or the bg canvas (links) */
+ setDirtyCanvas(fg: boolean, bg: boolean): void;
+ loadImage(url: string): void;
+ /** Allows to get onMouseMove and onMouseUp events even if the mouse is out of focus */
+ captureInput(v: any): void;
+ /** Collapse the node to make it smaller on the canvas */
+ collapse(force: boolean): void;
+ /** Forces the node to do not move or realign on Z */
+ pin(v?: boolean): void;
+ localToScreen(x: number, y: number, graphCanvas: LGraphCanvas): Vector2;
+
+ // https://github.com/jagenjo/litegraph.js/blob/master/guides/README.md#custom-node-appearance
+ onDrawBackground?(
+ ctx: CanvasRenderingContext2D,
+ // @rgthree fixed
+ canvas: LGraphCanvas
+ ): void;
+ onDrawForeground?(
+ ctx: CanvasRenderingContext2D,
+ // @rgthree fixed
+ canvas: LGraphCanvas
+ ): void;
+
+ // https://github.com/jagenjo/litegraph.js/blob/master/guides/README.md#custom-node-behaviour
+ onMouseDown?(
+ event: MouseEvent,
+ pos: Vector2,
+ graphCanvas: LGraphCanvas
+ ): void;
+ onMouseMove?(
+ event: MouseEvent,
+ pos: Vector2,
+ graphCanvas: LGraphCanvas
+ ): void;
+ onMouseUp?(
+ event: MouseEvent,
+ pos: Vector2,
+ graphCanvas: LGraphCanvas
+ ): void;
+ onMouseEnter?(
+ event: MouseEvent,
+ pos: Vector2,
+ graphCanvas: LGraphCanvas
+ ): void;
+ onMouseLeave?(
+ event: MouseEvent,
+ pos: Vector2,
+ graphCanvas: LGraphCanvas
+ ): void;
+ onKey?(event: KeyboardEvent, pos: Vector2, graphCanvas: LGraphCanvas): void;
+ // @rgthree
+ onKeyDown?(event: KeyboardEvent): void;
+ // @rgthree
+ onKeyUp?(event: KeyboardEvent): void;
+
+ onExecuted(message: any): void;
+ /** Called by `LGraphNode.createNode` */
+ onNodeCreated?(): void;
+ /** Called by `LGraphCanvas.selectNodes` */
+ onSelected?(): void;
+ /** Called by `LGraphCanvas.deselectNode` */
+ onDeselected?(): void;
+ /** Called by `LGraph.runStep` `LGraphNode.getInputData` */
+ onExecute?(): void;
+ /** Called by `LGraph.serialize` */
+ onSerialize?(o: SerializedLGraphNode): void;
+ /** Called by `LGraph.configure` */
+ onConfigure?(o: SerializedLGraphNode): void;
+ /**
+ * when added to graph (warning: this is called BEFORE the node is configured when loading)
+ * Called by `LGraph.add`
+ */
+ onAdded?(graph: LGraph): void;
+ /**
+ * when removed from graph
+ * Called by `LGraph.remove` `LGraph.clear`
+ */
+ onRemoved?(): void;
+ /**
+ * if returns false the incoming connection will be canceled
+ * Called by `LGraph.connect`
+ * @param inputIndex target input slot number
+ * @param outputType type of output slot
+ * @param outputSlot output slot object
+ * @param outputNode node containing the output
+ * @param outputIndex index of output slot
+ */
+ onConnectInput?(
+ inputIndex: number,
+ outputType: INodeOutputSlot["type"],
+ outputSlot: INodeOutputSlot,
+ outputNode: LGraphNode,
+ outputIndex: number
+ ): boolean;
+ /**
+ * if returns false the incoming connection will be canceled
+ * Called by `LGraph.connect`
+ * @param outputIndex target output slot number
+ * @param inputType type of input slot
+ * @param inputSlot input slot object
+ * @param inputNode node containing the input
+ * @param inputIndex index of input slot
+ */
+ onConnectOutput?(
+ outputIndex: number,
+ inputType: INodeInputSlot["type"],
+ inputSlot: INodeInputSlot,
+ inputNode: LGraphNode,
+ inputIndex: number
+ ): boolean;
+
+ /**
+ * Called just before connection (or disconnect - if input is linked).
+ * A convenient place to switch to another input, or create new one.
+ * This allow for ability to automatically add slots if needed
+ * @param inputIndex
+ * @return selected input slot index, can differ from parameter value
+ */
+ onBeforeConnectInput?(
+ inputIndex: number
+ ): number;
+
+ /** a connection changed (new one or removed) (LiteGraph.INPUT or LiteGraph.OUTPUT, slot, true if connected, link_info, input_info or output_info ) */
+ onConnectionsChange(
+ type: number,
+ slotIndex: number,
+ isConnected: boolean,
+ link: LLink,
+ // @rgthree - Make it INodeSlot instead of union
+ ioSlot: INodeSlot
+ ): void;
+
+ /**
+ * if returns false, will abort the `LGraphNode.setProperty`
+ * Called when a property is changed
+ * @param property
+ * @param value
+ * @param prevValue
+ */
+ onPropertyChanged?(property: string, value: any, prevValue: any): void | boolean;
+
+ /** Called by `LGraphCanvas.processContextMenu` */
+ getMenuOptions?(graphCanvas: LGraphCanvas): ContextMenuItem[];
+ // @rgthree. This is fixed because the INodeSlot is wrong and, also, null can be returned to not trigger a menu.
+ // getSlotMenuOptions?(slot: INodeSlot): ContextMenuItem[];
+ getSlotMenuOptions(slot: {input?: INodeInputSlot, output?: INodeOutputSlot}): ContextMenuItem[] | null;
+
+ getExtraMenuOptions?(canvas: LGraphCanvas, options: ContextMenuItem[]): void;
+
+ // @rgthree - Called in LiteGraph.core when the properties panel is constructed.
+ onShowCustomPanelInfo(panel: HTMLElement): void;
+}
+
+export type LGraphNodeConstructor = {
+ new (): T;
+
+ // @rgthree
+ title_mode?:
+ typeof LiteGraph.NORMAL_TITLE |
+ typeof LiteGraph.TRANSPARENT_TITLE |
+ typeof LiteGraph.AUTOHIDE_TITLE |
+ typeof LiteGraph.NO_TITLE;
+ title: string;
+ category: string;
+ type: string;
+ comfyClass?: string;
+};
+
+export type SerializedLGraphGroup = {
+ title: LGraphGroup["title"];
+ bounding: LGraphGroup["_bounding"];
+ color: LGraphGroup["color"];
+ font: LGraphGroup["font"];
+};
+export declare class LGraphGroup {
+ title: string;
+ // @rgthree - mark unprivate
+ _bounding: Vector4;
+ // @rgthree - updated to make optional because it seems to be so.
+ color?: string|null;
+ font: string;
+ // @rgthree
+ _nodes: LGraphNode[];
+ // @rgthree
+ _pos: Vector2;
+ // @rgthree
+ _size: Vector2;
+ // @rgthree
+ graph: LGraph;
+ // @rgthree - apparently it is available?
+ size: Vector2;
+ // @rgthree - apparently it is available?
+ pos: Vector2;
+
+
+ configure(o: SerializedLGraphGroup): void;
+ serialize(): SerializedLGraphGroup;
+ move(deltaX: number, deltaY: number, ignoreNodes?: boolean): void;
+ recomputeInsideNodes(): void;
+ isPointInside: LGraphNode["isPointInside"];
+ setDirtyCanvas: LGraphNode["setDirtyCanvas"];
+}
+
+export declare class DragAndScale {
+ constructor(element?: HTMLElement, skipEvents?: boolean);
+ offset: [number, number];
+ scale: number;
+ max_scale: number;
+ min_scale: number;
+ onredraw: Function | null;
+ enabled: boolean;
+ last_mouse: Vector2;
+ element: HTMLElement | null;
+ visible_area: Vector4;
+ bindEvents(element: HTMLElement): void;
+ computeVisibleArea(): void;
+ onMouse(e: MouseEvent): void;
+ toCanvasContext(ctx: CanvasRenderingContext2D): void;
+ convertOffsetToCanvas(pos: Vector2): Vector2;
+ convertCanvasToOffset(pos: Vector2): Vector2;
+ mouseDrag(x: number, y: number): void;
+ changeScale(value: number, zooming_center?: Vector2): void;
+ changeDeltaScale(value: number, zooming_center?: Vector2): void;
+ reset(): void;
+}
+
+// @rgthree.
+interface CanvasDivDialog extends HTMLDivElement {
+ close: () => void;
+ modified: () => void;
+ is_modified: boolean;
+}
+
+/**
+ * This class is in charge of rendering one graph inside a canvas. And provides all the interaction required.
+ * Valid callbacks are: onNodeSelected, onNodeDeselected, onShowNodePanel, onNodeDblClicked
+ *
+ * @param canvas the canvas where you want to render (it accepts a selector in string format or the canvas element itself)
+ * @param graph
+ * @param options { skip_rendering, autoresize }
+ */
+export declare class LGraphCanvas {
+ static node_colors: Record<
+ string,
+ {
+ color: string;
+ bgcolor: string;
+ groupcolor: string;
+ }
+ >;
+ static link_type_colors: Record;
+ static gradients: object;
+ static search_limit: number;
+
+ static getFileExtension(url: string): string;
+ static decodeHTML(str: string): string;
+
+ static onMenuCollapseAll(): void;
+ static onMenuNodeEdit(): void;
+ static onShowPropertyEditor(
+ item: any,
+ options: any,
+ e: any,
+ menu: any,
+ node: any
+ ): void;
+ /** Create menu for `Add Group` */
+ static onGroupAdd: ContextMenuEventListener;
+ /** Create menu for `Add Node` */
+ static onMenuAdd: ContextMenuEventListener;
+ static showMenuNodeOptionalInputs: ContextMenuEventListener;
+ static showMenuNodeOptionalOutputs: ContextMenuEventListener;
+ static onShowMenuNodeProperties: ContextMenuEventListener;
+ static onResizeNode: ContextMenuEventListener;
+ static onMenuNodeCollapse: ContextMenuEventListener;
+ static onMenuNodePin: ContextMenuEventListener;
+ static onMenuNodeMode: ContextMenuEventListener;
+ static onMenuNodeColors: ContextMenuEventListener;
+ static onMenuNodeShapes: ContextMenuEventListener;
+ static onMenuNodeRemove: ContextMenuEventListener;
+ static onMenuNodeClone: ContextMenuEventListener;
+
+ // @rgthree
+ static onShowPropertyEditor: ContextMenuEventListener;
+
+ constructor(
+ canvas: HTMLCanvasElement | string,
+ graph?: LGraph,
+ options?: {
+ skip_render?: boolean;
+ autoresize?: boolean;
+ }
+ );
+
+ // @rgthree. This was "HTMLCanvasElement" but that is just wrong... it's LGraphCanvas
+ static active_canvas: LGraphCanvas;
+
+ // @rgthree
+ pointer_is_down: boolean;
+
+ allow_dragcanvas: boolean;
+ allow_dragnodes: boolean;
+ /** allow to control widgets, buttons, collapse, etc */
+ allow_interaction: boolean;
+ /** allows to change a connection with having to redo it again */
+ allow_reconnect_links: boolean;
+ /** allow selecting multi nodes without pressing extra keys */
+ multi_select: boolean;
+ /** No effect */
+ allow_searchbox: boolean;
+ always_render_background: boolean;
+ autoresize?: boolean;
+ background_image: string;
+ bgcanvas: HTMLCanvasElement;
+ bgctx: CanvasRenderingContext2D;
+ canvas: HTMLCanvasElement;
+ canvas_mouse: Vector2;
+ // @rgthree - Looks like this is to replace canvas_mouse.
+ graph_mouse: Vector2;
+ clear_background: boolean;
+ // connecting_node: LGraphNode | null;
+ // // @rgthree - for overriding.
+ // _connecting_node: LGraphNode | null;
+ // @rgthree
+ // connecting_input: INodeInputSlot | null;
+ // // @rgthree
+ // connecting_output: INodeOutputSlot | null;
+ // // @rgthree
+ // connecting_slot: number;
+ // // @rgthree
+ // connecting_pos: Vector2 | null;
+ // @rgthree - for some reason, the new comfyUI update renamed connecting_node to connecting_links (maybe)
+ connecting_links: {
+ node: LGraphNode, input?: INodeInputSlot, output?: INodeOutputSlot, pos: Vector2, slot: number
+ }[] | null;
+ _connecting_links: this['connecting_links'] | null;
+
+ connections_width: number;
+ ctx: CanvasRenderingContext2D;
+ current_node: LGraphNode | null;
+ default_connection_color: {
+ input_off: string;
+ input_on: string;
+ output_off: string;
+ output_on: string;
+ };
+ default_link_color: string;
+ dirty_area: Vector4 | null;
+ dirty_bgcanvas?: boolean;
+ dirty_canvas?: boolean;
+ drag_mode: boolean;
+ dragging_canvas: boolean;
+ dragging_rectangle: Vector4 | null;
+ // @rgthree; mark undefined.
+ // It doesn't look like this should ever be undefined.. but something changed in Comfy and folks
+ // reported https://github.com/rgthree/rgthree-comfy/issues/71. I couldn't reproduce, but we can
+ // handle it.
+ ds?: DragAndScale;
+ /** used for transition */
+ editor_alpha: number;
+ filter: any;
+ fps: number;
+ frame: number;
+ graph: LGraph;
+ highlighted_links: Record;
+ highquality_render: boolean;
+ inner_text_font: string;
+ is_rendering: boolean;
+ last_draw_time: number;
+ last_mouse: Vector2;
+ /**
+ * Possible duplicated with `last_mouse`
+ * https://github.com/jagenjo/litegraph.js/issues/70
+ */
+ last_mouse_position: Vector2;
+ /** Timestamp of last mouse click, defaults to 0 */
+ last_mouseclick: number;
+ links_render_mode:
+ | typeof LiteGraph.STRAIGHT_LINK
+ | typeof LiteGraph.LINEAR_LINK
+ | typeof LiteGraph.SPLINE_LINK;
+ live_mode: boolean;
+ node_capturing_input: LGraphNode | null;
+ node_dragged: LGraphNode | null;
+ node_in_panel: LGraphNode | null;
+ node_over: LGraphNode | null;
+ node_title_color: string;
+ node_widget: [LGraphNode, IWidget] | null;
+ /** Called by `LGraphCanvas.drawBackCanvas` */
+ onDrawBackground:
+ | ((ctx: CanvasRenderingContext2D, visibleArea: Vector4) => void)
+ | null;
+ /** Called by `LGraphCanvas.drawFrontCanvas` */
+ onDrawForeground:
+ | ((ctx: CanvasRenderingContext2D, visibleArea: Vector4) => void)
+ | null;
+ onDrawOverlay: ((ctx: CanvasRenderingContext2D) => void) | null;
+ /** Called by `LGraphCanvas.processMouseDown` */
+ onMouse: ((event: MouseEvent) => boolean) | null;
+ /** Called by `LGraphCanvas.drawFrontCanvas` and `LGraphCanvas.drawLinkTooltip` */
+ onDrawLinkTooltip: ((ctx: CanvasRenderingContext2D, link: LLink, _this: this) => void) | null;
+ /** Called by `LGraphCanvas.selectNodes` */
+ onNodeMoved: ((node: LGraphNode) => void) | null;
+ /** Called by `LGraphCanvas.processNodeSelected` */
+ onNodeSelected: ((node: LGraphNode) => void) | null;
+ /** Called by `LGraphCanvas.deselectNode` */
+ onNodeDeselected: ((node: LGraphNode) => void) | null;
+ /** Called by `LGraphCanvas.processNodeDblClicked` */
+ onShowNodePanel: ((node: LGraphNode) => void) | null;
+ /** Called by `LGraphCanvas.processNodeDblClicked` */
+ onNodeDblClicked: ((node: LGraphNode) => void) | null;
+ /** Called by `LGraphCanvas.selectNodes` */
+ onSelectionChange: ((nodes: Record) => void) | null;
+ /** Called by `LGraphCanvas.showSearchBox` */
+ onSearchBox:
+ | ((
+ helper: Element,
+ value: string,
+ graphCanvas: LGraphCanvas
+ ) => string[])
+ | null;
+ onSearchBoxSelection:
+ | ((name: string, event: MouseEvent, graphCanvas: LGraphCanvas) => void)
+ | null;
+ pause_rendering: boolean;
+ render_canvas_border: boolean;
+ render_collapsed_slots: boolean;
+ render_connection_arrows: boolean;
+ render_connections_border: boolean;
+ render_connections_shadows: boolean;
+ render_curved_connections: boolean;
+ render_execution_order: boolean;
+ render_only_selected: boolean;
+ render_shadows: boolean;
+ render_title_colored: boolean;
+ round_radius: number;
+ selected_group: null | LGraphGroup;
+ selected_group_resizing: boolean;
+ selected_nodes: Record;
+ show_info: boolean;
+ title_text_font: string;
+ /** set to true to render title bar with gradients */
+ use_gradients: boolean;
+ visible_area: DragAndScale["visible_area"];
+ visible_links: LLink[];
+ visible_nodes: LGraphNode[];
+ zoom_modify_alpha: boolean;
+
+ /** clears all the data inside */
+ clear(): void;
+ /** assigns a graph, you can reassign graphs to the same canvas */
+ setGraph(graph: LGraph, skipClear?: boolean): void;
+ /** opens a graph contained inside a node in the current graph */
+ openSubgraph(graph: LGraph): void;
+ /** closes a subgraph contained inside a node */
+ closeSubgraph(): void;
+ /** assigns a canvas */
+ setCanvas(canvas: HTMLCanvasElement, skipEvents?: boolean): void;
+ /** binds mouse, keyboard, touch and drag events to the canvas */
+ bindEvents(): void;
+ /** unbinds mouse events from the canvas */
+ unbindEvents(): void;
+
+ /**
+ * this function allows to render the canvas using WebGL instead of Canvas2D
+ * this is useful if you plant to render 3D objects inside your nodes, it uses litegl.js for webgl and canvas2DtoWebGL to emulate the Canvas2D calls in webGL
+ **/
+ enableWebGL(): void;
+
+ /**
+ * marks as dirty the canvas, this way it will be rendered again
+ * @param fg if the foreground canvas is dirty (the one containing the nodes)
+ * @param bg if the background canvas is dirty (the one containing the wires)
+ */
+ setDirty(fg: boolean, bg: boolean): void;
+
+ /**
+ * Used to attach the canvas in a popup
+ * @return the window where the canvas is attached (the DOM root node)
+ */
+ getCanvasWindow(): Window;
+ /** starts rendering the content of the canvas when needed */
+ startRendering(): void;
+ /** stops rendering the content of the canvas (to save resources) */
+ stopRendering(): void;
+
+ processMouseDown(e: MouseEvent): boolean | undefined;
+ processMouseMove(e: MouseEvent): boolean | undefined;
+ processMouseUp(e: MouseEvent): boolean | undefined;
+ processMouseWheel(e: MouseEvent): boolean | undefined;
+
+ /** returns true if a position (in graph space) is on top of a node little corner box */
+ isOverNodeBox(node: LGraphNode, canvasX: number, canvasY: number): boolean;
+ /** returns true if a position (in graph space) is on top of a node input slot */
+ isOverNodeInput(
+ node: LGraphNode,
+ canvasX: number,
+ canvasY: number,
+ slotPos: Vector2
+ ): boolean;
+
+ /** process a key event */
+ processKey(e: KeyboardEvent): boolean | undefined;
+
+ // @rgthree - added param
+ copyToClipboard(nodes: LGraphNode[]|{[key:number]:LGraphNode}): void;
+ pasteFromClipboard(): void;
+ processDrop(e: DragEvent): void;
+ checkDropItem(e: DragEvent): void;
+ processNodeDblClicked(n: LGraphNode): void;
+ processNodeSelected(n: LGraphNode, e: MouseEvent): void;
+ processNodeDeselected(node: LGraphNode): void;
+
+ /** selects a given node (or adds it to the current selection) */
+ selectNode(node: LGraphNode, add?: boolean): void;
+ /** selects several nodes (or adds them to the current selection) */
+ selectNodes(nodes?: LGraphNode[], add?: boolean): void;
+ /** removes a node from the current selection */
+ deselectNode(node: LGraphNode): void;
+ /** removes all nodes from the current selection */
+ deselectAllNodes(): void;
+ /** deletes all nodes in the current selection from the graph */
+ deleteSelectedNodes(): void;
+
+ /** centers the camera on a given node */
+ // @rgthree - narrow parameter
+ centerOnNode(node: {pos: Vector2, size: Vector2}): void;
+ /** changes the zoom level of the graph (default is 1), you can pass also a place used to pivot the zoom */
+ setZoom(value: number, center: Vector2): void;
+ /** brings a node to front (above all other nodes) */
+ bringToFront(node: LGraphNode): void;
+ /** sends a node to the back (below all other nodes) */
+ sendToBack(node: LGraphNode): void;
+ /** checks which nodes are visible (inside the camera area) */
+ computeVisibleNodes(nodes: LGraphNode[]): LGraphNode[];
+ /** renders the whole canvas content, by rendering in two separated canvas, one containing the background grid and the connections, and one containing the nodes) */
+ draw(forceFG?: boolean, forceBG?: boolean): void;
+ /** draws the front canvas (the one containing all the nodes) */
+ drawFrontCanvas(): void;
+ /** draws some useful stats in the corner of the canvas */
+ renderInfo(ctx: CanvasRenderingContext2D, x: number, y: number): void;
+ /** draws the back canvas (the one containing the background and the connections) */
+ drawBackCanvas(): void;
+ /** draws the given node inside the canvas */
+ drawNode(node: LGraphNode, ctx: CanvasRenderingContext2D): void;
+ /** draws graphic for node's slot */
+ drawSlotGraphic(ctx: CanvasRenderingContext2D, pos: number[], shape: SlotShape, horizontal: boolean): void;
+ /** draws the shape of the given node in the canvas */
+ drawNodeShape(
+ node: LGraphNode,
+ ctx: CanvasRenderingContext2D,
+ size: [number, number],
+ fgColor: string,
+ bgColor: string,
+ selected: boolean,
+ mouseOver: boolean
+ ): void;
+ /** draws every connection visible in the canvas */
+ drawConnections(ctx: CanvasRenderingContext2D): void;
+ /**
+ * draws a link between two points
+ * @param a start pos
+ * @param b end pos
+ * @param link the link object with all the link info
+ * @param skipBorder ignore the shadow of the link
+ * @param flow show flow animation (for events)
+ * @param color the color for the link
+ * @param startDir the direction enum
+ * @param endDir the direction enum
+ * @param numSublines number of sublines (useful to represent vec3 or rgb)
+ **/
+ renderLink(
+ a: Vector2,
+ b: Vector2,
+ link: object,
+ skipBorder: boolean,
+ flow: boolean,
+ color?: string,
+ startDir?: number,
+ endDir?: number,
+ numSublines?: number
+ ): void;
+
+ computeConnectionPoint(
+ a: Vector2,
+ b: Vector2,
+ t: number,
+ startDir?: number,
+ endDir?: number
+ ): void;
+
+ drawExecutionOrder(ctx: CanvasRenderingContext2D): void;
+ /** draws the widgets stored inside a node */
+ drawNodeWidgets(
+ node: LGraphNode,
+ posY: number,
+ ctx: CanvasRenderingContext2D,
+ activeWidget: object
+ ): void;
+ /** process an event on widgets */
+ processNodeWidgets(
+ node: LGraphNode,
+ pos: Vector2,
+ event: Event,
+ activeWidget: object
+ ): void;
+ /** draws every group area in the background */
+ drawGroups(canvas: any, ctx: CanvasRenderingContext2D): void;
+ adjustNodesSize(): void;
+ /** resizes the canvas to a given size, if no size is passed, then it tries to fill the parentNode */
+ resize(width?: number, height?: number): void;
+ /**
+ * switches to live mode (node shapes are not rendered, only the content)
+ * this feature was designed when graphs where meant to create user interfaces
+ **/
+ switchLiveMode(transition?: boolean): void;
+ onNodeSelectionChange(): void;
+ touchHandler(event: TouchEvent): void;
+
+ showLinkMenu(link: LLink, e: any): false;
+ prompt(
+ title: string,
+ value: any,
+ callback: Function,
+ event: any
+ ): HTMLDivElement;
+ showSearchBox(event?: MouseEvent): void;
+ showEditPropertyValue(node: LGraphNode, property: any, options: any): void;
+ createDialog(
+ html: string,
+ options?: { position?: Vector2; event?: MouseEvent }
+ // @rgthree - Fix return type from void (added above)
+ ): CanvasDivDialog;
+
+
+
+ convertOffsetToCanvas: DragAndScale["convertOffsetToCanvas"];
+ convertCanvasToOffset: DragAndScale["convertCanvasToOffset"];
+ /** converts event coordinates from canvas2D to graph coordinates */
+ // @rgthree - change MouseEvent to less restrictive {clientX: number, clientY: number} that
+ // implementation uses.
+ convertEventToCanvasOffset(e: {clientX: number, clientY: number}): Vector2;
+ /** adds some useful properties to a mouse event, like the position in graph coordinates */
+ adjustMouseEvent(e: MouseEvent): void;
+
+ getCanvasMenuOptions(): ContextMenuItem[];
+ getNodeMenuOptions(node: LGraphNode): ContextMenuItem[];
+ getGroupMenuOptions(): ContextMenuItem[];
+ /** Called by `getCanvasMenuOptions`, replace default options */
+ getMenuOptions?(): ContextMenuItem[];
+ /** Called by `getCanvasMenuOptions`, append to default options */
+ getExtraMenuOptions?(): ContextMenuItem[];
+ /** Called when mouse right click */
+ processContextMenu(node: LGraphNode, event: Event): void;
+
+ // @rgthree - Adding this for ComfyUI, since they add this in their own overload in app.js
+ selected_group_moving?: boolean;
+
+ // @rgthree
+ showShowNodePanel(node: LGraphNode): void;
+}
+
+// @rgthree - The adjusted pointer event after calling adjustMouseEvent
+export interface AdjustedMouseEvent extends PointerEvent {
+ deltaX: number;
+ deltaY: number;
+ canvasX: number;
+ canvasY: number;
+}
+
+declare class ContextMenu {
+ static trigger(
+ element: HTMLElement,
+ event_name: string,
+ params: any,
+ origin: any
+ ): void;
+ static isCursorOverElement(event: MouseEvent, element: HTMLElement): void;
+ static closeAllContextMenus(window: Window): void;
+ constructor(values: ContextMenuItem[]|string[], options?: IContextMenuOptions, window?: Window);
+ options: IContextMenuOptions;
+ parentMenu?: ContextMenu;
+ lock: boolean;
+ current_submenu?: ContextMenu;
+ addItem(
+ name: string,
+ value: ContextMenuItem,
+ options?: IContextMenuOptions
+ ): void;
+ close(e?: MouseEvent, ignore_parent_menu?: boolean): void;
+ getTopMenu(): void;
+ getFirstEvent(): void;
+}
+
+declare global {
+ interface Math {
+ clamp(v: number, min: number, max: number): number;
+ }
+}
diff --git a/web/common.d.ts b/web/common.d.ts
index 8685b36..6c4101a 100644
--- a/web/common.d.ts
+++ b/web/common.d.ts
@@ -1,3 +1,4 @@
-import type { ComfyApp, TLGraphNode } from './liteGraph.js';
+import type { TLGraphNode } from './comfy/index.js';
+import { ComfyApp } from './comfy/index.js';
export declare const commonPrefix = "\uD83E\uDE9B";
export declare function displayContext(nodeType: TLGraphNode, appFromArg: ComfyApp, index?: number, serialize_widgets?: boolean, isVirtualNode?: boolean): void;
diff --git a/web/common.js b/web/common.js
index 19c221b..1cceb83 100644
--- a/web/common.js
+++ b/web/common.js
@@ -1,4 +1,4 @@
-import { ComfyWidgets } from './liteGraph.js';
+import { ComfyWidgets } from './comfy/index.js';
export const commonPrefix = '🪛';
export function displayContext(nodeType, appFromArg, index = 0, serialize_widgets = false, isVirtualNode = false) {
function populate(text) {
@@ -14,9 +14,7 @@ export function displayContext(nodeType, appFromArg, index = 0, serialize_widget
this.serialize_widgets = serialize_widgets;
this.isVirtualNode = isVirtualNode;
const widget = ComfyWidgets.STRING(this, 'text', [
- 'STRING', {
- multiline: true,
- },
+ 'STRING', { multiline: true },
], appFromArg).widget;
widget.inputEl.readOnly = true;
widget.inputEl.style.opacity = 0.6;
@@ -50,10 +48,3 @@ export function displayContext(nodeType, appFromArg, index = 0, serialize_widget
}
};
}
-const style = document.createElement('style');
-style.innerHTML = `
- #comfy-settings-dialog label[for^=Crystools-] {
- white-space: pre-line;
- }
-`;
-document.head.appendChild(style);
diff --git a/web/common.ts b/web/common.ts
index af3c3f3..a221b25 100644
--- a/web/common.ts
+++ b/web/common.ts
@@ -1,5 +1,5 @@
-import type { ComfyApp, TLGraphNode } from './liteGraph.js';
-import { ComfyWidgets } from './liteGraph.js';
+import type { TLGraphNode } from './comfy/index.js';
+import { ComfyWidgets, ComfyApp } from './comfy/index.js';
export const commonPrefix = '🪛';
@@ -25,9 +25,7 @@ export function displayContext(
this.isVirtualNode = isVirtualNode;
const widget = ComfyWidgets.STRING(this, 'text', [
- 'STRING', {
- multiline: true,
- },
+ 'STRING', { multiline: true },
], appFromArg).widget;
widget.inputEl.readOnly = true;
widget.inputEl.style.opacity = 0.6;
@@ -53,10 +51,12 @@ export function displayContext(
}
// When the node is executed we will be sent the input text, display this in the widget
+
// @ts-ignore
+ // eslint-disable-next-line @typescript-eslint/unbound-method
const onExecutedOriginal = nodeType.prototype.onExecuted;
- // @ts-ignore
nodeType.prototype.onExecuted = function(message: { text: string }): void {
+ // @ts-ignore
onExecutedOriginal?.apply(this, arguments);
populate.call(this, message.text);
};
@@ -90,13 +90,3 @@ export function displayContext(
// }
// }
// };
-
-// HACKS
-// for settings using break lines on label
-const style = document.createElement('style');
-style.innerHTML = `
- #comfy-settings-dialog label[for^=Crystools-] {
- white-space: pre-line;
- }
-`;
-document.head.appendChild(style);
diff --git a/web/debugger.js b/web/debugger.js
index 50596bd..395edd2 100644
--- a/web/debugger.js
+++ b/web/debugger.js
@@ -1,7 +1,5 @@
-import { app } from '/scripts/app.js';
-import { api } from '/scripts/api.js';
+import { app, api, ComfyWidgets, LiteGraph, TLGraphNode } from './comfy/index.js';
import { commonPrefix, displayContext } from './common.js';
-import { LiteGraph, TLGraphNode, ComfyWidgets } from './liteGraph.js';
app.registerExtension({
name: 'Crystools.Debugger.ConsoleAny',
beforeRegisterNodeDef(nodeType, nodeData, appFromArg) {
@@ -20,7 +18,7 @@ app.registerExtension({
enumerable: true,
configurable: true,
writable: true,
- value: async () => {
+ value: () => {
return app.graphToPrompt()
.then((workflow) => {
let result = 'inactive';
@@ -56,25 +54,17 @@ app.registerExtension({
this.serialize_widgets = false;
this.isVirtualNode = true;
const widget = ComfyWidgets.STRING(this, '', [
- '', {
- default: '', multiline: true,
- },
+ '', { default: '', multiline: true },
], app).widget;
widget.inputEl.readOnly = true;
ComfyWidgets.BOOLEAN(this, 'Active', [
- '', {
- default: true,
- },
+ '', { default: true },
]);
ComfyWidgets.BOOLEAN(this, 'Parsed', [
- '', {
- default: true,
- },
+ '', { default: true },
]);
ComfyWidgets.COMBO(this, 'What', [
- ['Prompt', 'Workflow'], {
- default: 'Prompt',
- },
+ ['Prompt', 'Workflow'], { default: 'Prompt' },
]);
api.addEventListener('executed', this.fillMetadataWidget, false);
}
diff --git a/web/debugger.ts b/web/debugger.ts
index 017bc29..b37f8a2 100644
--- a/web/debugger.ts
+++ b/web/debugger.ts
@@ -1,8 +1,6 @@
-import { app } from '/scripts/app.js';
-import { api } from '/scripts/api.js';
+import type { ComfyNode } from './comfy/index.js';
+import { app, api, ComfyWidgets, LiteGraph, TLGraphNode, ComfyApp } from './comfy/index.js';
import { commonPrefix, displayContext } from './common.js';
-import type { ComfyNode, ComfyApp } from './liteGraph.js';
-import { LiteGraph, TLGraphNode, ComfyWidgets } from './liteGraph.js';
// "Show any" Node
app.registerExtension({
@@ -25,36 +23,28 @@ app.registerExtension({
this.isVirtualNode = true;
const widget = ComfyWidgets.STRING(this, '', [
- '', {
- default: '', multiline: true,
- },
+ '', {default: '', multiline: true},
], app).widget;
widget.inputEl.readOnly = true;
ComfyWidgets.BOOLEAN(this, 'Active', [
- '', {
- default: true,
- },
+ '', {default: true},
]);
ComfyWidgets.BOOLEAN(this, 'Parsed', [
- '', {
- default: true,
- },
+ '', {default: true},
]);
ComfyWidgets.COMBO(this, 'What', [
- ['Prompt', 'Workflow'], {
- default: 'Prompt',
- },
+ ['Prompt', 'Workflow'], {default: 'Prompt'},
]);
// It runs at finish on each prompt queue
api.addEventListener('executed', this.fillMetadataWidget, false);
}
- fillMetadataWidget = async (): Promise => {
+ fillMetadataWidget = (): Promise => {
return app.graphToPrompt()
- .then((workflow): string => {
+ .then((workflow: any): string => {
let result = 'inactive';
- if(this.widgets?.length !== 4) {
+ if (this.widgets?.length !== 4) {
console.error('Something is wrong with the widgets, should be 4!');
return 'error';
}
diff --git a/web/extensions.js b/web/extensions.js
index 15a9f68..0c78b49 100644
--- a/web/extensions.js
+++ b/web/extensions.js
@@ -1,5 +1,9 @@
-import { app } from '/scripts/app.js';
+import { app } from './comfy/index.js';
import { displayContext } from './common.js';
+const crystoolsExtensionsSerialized = {
+ 'Read JSON file [Crystools]': 'Crystools.Utils.ReadJsonFile',
+ 'JSON extractor [Crystools]': 'Crystools.Utils.JsonExtractor',
+};
const crystoolsExtensions = {
'Get resolution [Crystools]': 'Crystools.Image.GetResolution',
'Preview from image [Crystools]': 'Crystools.Image.PreviewFromImage',
@@ -8,12 +12,20 @@ const crystoolsExtensions = {
'Stats system [Crystools]': 'Crystools.Utils.StatsSystem',
'Show any to JSON [Crystools]': 'Crystools.Debugger.ConsoleAnyToJson',
};
+Object.keys(crystoolsExtensionsSerialized).forEach(prop => {
+ crystoolsExtensions[prop] = crystoolsExtensionsSerialized[prop];
+});
Object.keys(crystoolsExtensions).forEach(key => {
app.registerExtension({
name: crystoolsExtensions[key],
beforeRegisterNodeDef(nodeType, nodeData, appFromArg) {
if (nodeData.name === key) {
- displayContext(nodeType, appFromArg, 0);
+ if (nodeData.name in crystoolsExtensionsSerialized) {
+ displayContext(nodeType, appFromArg, 0, true);
+ }
+ else {
+ displayContext(nodeType, appFromArg, 0);
+ }
}
},
});
diff --git a/web/extensions.ts b/web/extensions.ts
index 25a6950..533c92d 100644
--- a/web/extensions.ts
+++ b/web/extensions.ts
@@ -1,7 +1,12 @@
-import { app } from '/scripts/app.js';
+import { app, TLGraphNode, ComfyApp } from './comfy/index.js';
+import type { ComfyNode } from './comfy/index.js';
import { displayContext } from './common.js';
-import type { ComfyNode, ComfyApp } from './liteGraph.types';
-import { TLGraphNode } from './liteGraph';
+
+const crystoolsExtensionsSerialized: Record = {
+ // 'External parameter from JSON file [Crystools]': 'Crystools.Utils.ExternalParameterFromJson',
+ 'Read JSON file [Crystools]': 'Crystools.Utils.ReadJsonFile',
+ 'JSON extractor [Crystools]': 'Crystools.Utils.JsonExtractor',
+};
const crystoolsExtensions: Record = {
'Get resolution [Crystools]': 'Crystools.Image.GetResolution',
@@ -12,12 +17,21 @@ const crystoolsExtensions: Record = {
'Show any to JSON [Crystools]': 'Crystools.Debugger.ConsoleAnyToJson',
};
+Object.keys(crystoolsExtensionsSerialized).forEach(prop => {
+ // @ts-ignore
+ crystoolsExtensions[prop] = crystoolsExtensionsSerialized[prop];
+});
+
Object.keys(crystoolsExtensions).forEach(key => {
app.registerExtension({
name: crystoolsExtensions[key],
beforeRegisterNodeDef(nodeType: ComfyNode, nodeData: TLGraphNode, appFromArg: ComfyApp) {
if (nodeData.name === key) {
- displayContext(nodeType, appFromArg, 0);
+ if (nodeData.name in crystoolsExtensionsSerialized) {
+ displayContext(nodeType, appFromArg, 0, true); // serialize_widgets = true
+ } else {
+ displayContext(nodeType, appFromArg, 0);
+ }
}
},
});
diff --git a/web/monitor.css b/web/monitor.css
new file mode 100644
index 0000000..8988f65
--- /dev/null
+++ b/web/monitor.css
@@ -0,0 +1,179 @@
+/* Vertical UI */
+.comfy-menu {
+ #crystools-monitors-root {
+ /*background-color: red;*/
+ box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.3);
+ background-color: var(--comfy-menu-bg);
+ border-radius: 6px;
+ border: 1px solid var(--border-color);
+ display: flex;
+ width: 100%;
+ flex-direction: column;
+ margin: 7px;
+ padding: 5px 0;
+ cursor: crosshair;
+
+ /* MONITORS */
+
+ .crystools-monitor {
+ margin: 1px 10px;
+ height: 12px;
+ display: flex;
+ align-items: center;
+
+ .crystools-text {
+ width: 40px;
+ font-size: 10px;
+ text-align: right;
+ margin-right: 3px;
+
+ &:hover {
+ font-weight: 600;
+ color: var(--input-text);
+ }
+ }
+
+ .crystools-label {
+ &:hover {
+ font-weight: 700;
+ color: var(--input-text);
+ }
+ }
+ }
+
+ .crystools-content {
+ flex-grow: 1;
+ position: relative;
+ background-color: var(--content-hover-bg)
+ }
+
+ .crystools-slider {
+ position: absolute;
+ height: 100%;
+ width: 0;
+ box-shadow: inset 2px 2px 10px rgba(0, 0, 0, 0.2);
+ }
+
+ .crystools-label {
+ position: relative;
+ color: var(--input-text);
+ font-size: 10px;
+ }
+ }
+
+ /* PROGRESS BAR */
+
+ #crystools-progressBar-root {
+ cursor: pointer;
+ margin: 0 12px 4px;
+ height: 18px;
+ position: relative;
+ border-radius: 3px;
+ width: 100%;
+ background-color: var(--comfy-input-bg);
+
+ .crystools-slider {
+ position: absolute;
+ height: 100%;
+ width: 50%;
+ transition: width 0.2s;
+ background-color: green;
+ box-shadow: inset 2px 2px 10px rgba(0, 0, 0, 0.3);
+ }
+
+ .crystools-label {
+ position: absolute;
+ margin: auto 0;
+ width: 100%;
+ color: var(--input-text);
+ font-size: 14px;
+ }
+ }
+}
+
+.comfyui-queue-button {
+ /*width: unset !important;*/
+}
+
+/* Horizontal UI */
+.comfyui-menu #crystools-monitors-root {
+ /*background-color: red;*/
+ /*display: none;*/
+ flex-direction: row;
+ /*flex-grow: 5;*/
+ justify-content: flex-end;
+ /*flex: auto;*/
+ flex-shrink: 1;
+ /*min-width: max-content;*/
+ /*height: 100%;*/
+ /* max-width: 40vw;*/
+ /* min-width: 10vw;*/
+ /*gap: 5px;*/
+ /*margin: 0 auto;*/
+ /*align-items: center;*/
+ /*align-self: center;*/
+ /*width: min-content;*/
+ /*width: 100%;*/
+ cursor: crosshair;
+ /*margin: 4px 0;*/
+ display: flex; /* by progress bar base */
+ flex-wrap: wrap;
+ /*flex-direction: row;*/
+ /*justify-content: flex-end;*/
+ /*align-items: center;*/
+ gap: 5px;
+ align-self: center;
+
+ .crystools-monitor {
+ background-color: var(--comfy-input-bg);
+ position: relative;
+ align-items: center;
+ flex-direction: row;
+ /*width: 60px;*/
+ /*height: 100%;*/
+
+ .crystools-text {
+ font-size: 10px;
+ text-align: right;
+ margin-left: 3px;
+ position: absolute;
+ font-weight: 100;
+ bottom: 2px;
+ z-index: 10;
+
+ /*&:hover {*/
+ /* font-weight: 600;*/
+ /* color: var(--input-text);*/
+ /*}*/
+ }
+
+ /*.crystools-label {*/
+ /* &:hover {*/
+ /* font-weight: 800;*/
+ /* color: var(--input-text);*/
+ /* }*/
+ /*}*/
+ }
+
+ .crystools-content {
+ position: relative;
+ }
+
+ .crystools-slider {
+ position: absolute;
+ height: 100%;
+ width: 0;
+ box-shadow: inset 2px 2px 10px rgba(0, 0, 0, 0.2);
+ }
+
+ .crystools-label {
+ position: relative;
+ width: 100%;
+ color: var(--input-text);
+ font-weight: 500;
+ font-size: 11px;
+ right: 2px;
+ top: 2px;
+ text-align: right;
+ }
+}
diff --git a/web/monitor.js b/web/monitor.js
index 2d09b49..a093c0e 100644
--- a/web/monitor.js
+++ b/web/monitor.js
@@ -1,6 +1,9 @@
-import { app } from '/scripts/app.js';
-import { api } from '/scripts/api.js';
+import { app, api, ComfyButtonGroup } from './comfy/index.js';
import { commonPrefix } from './common.js';
+import { MonitorUI } from './monitorUI.js';
+import { Colors } from './styles.js';
+import { convertNumberToPascalCase } from './utils.js';
+import { ComfyKeyMenuDisplayOption, MenuDisplayOptions } from './progressBarUIBase.js';
class CrystoolsMonitor {
constructor() {
Object.defineProperty(this, "idExtensionName", {
@@ -15,41 +18,59 @@ class CrystoolsMonitor {
writable: true,
value: commonPrefix
});
- Object.defineProperty(this, "htmlIdCrystoolsRoot", {
+ Object.defineProperty(this, "menuDisplayOption", {
enumerable: true,
configurable: true,
writable: true,
- value: 'crystools-root'
+ value: MenuDisplayOptions.Disabled
});
- Object.defineProperty(this, "htmlIdCrystoolsMonitorContainer", {
+ Object.defineProperty(this, "crystoolsButtonGroup", {
enumerable: true,
configurable: true,
writable: true,
- value: 'crystools-monitor-container'
+ value: null
});
- Object.defineProperty(this, "idInputRate", {
+ Object.defineProperty(this, "settingsRate", {
enumerable: true,
configurable: true,
writable: true,
- value: 'Crystools.inputRate'
+ value: void 0
});
- Object.defineProperty(this, "defaultRate", {
+ Object.defineProperty(this, "settingsMonitorHeight", {
enumerable: true,
configurable: true,
writable: true,
- value: .5
+ value: void 0
});
- Object.defineProperty(this, "idWhichHDD", {
+ Object.defineProperty(this, "settingsMonitorWidth", {
enumerable: true,
configurable: true,
writable: true,
- value: 'Crystools.whichHDD'
+ value: void 0
});
- Object.defineProperty(this, "defaultWhichHDD", {
+ Object.defineProperty(this, "monitorCPUElement", {
enumerable: true,
configurable: true,
writable: true,
- value: '/'
+ value: void 0
+ });
+ Object.defineProperty(this, "monitorRAMElement", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: void 0
+ });
+ Object.defineProperty(this, "monitorHDDElement", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: void 0
+ });
+ Object.defineProperty(this, "settingsHDD", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: void 0
});
Object.defineProperty(this, "monitorGPUSettings", {
enumerable: true,
@@ -69,95 +90,53 @@ class CrystoolsMonitor {
writable: true,
value: []
});
- Object.defineProperty(this, "monitorCPUElement", {
+ Object.defineProperty(this, "monitorUI", {
enumerable: true,
configurable: true,
writable: true,
- value: {
- id: 'Crystools.switchCPU',
- name: this.menuPrefix + ' [monitor] CPU Usage',
- type: 'boolean',
- label: 'CPU',
- symbol: '%',
- defaultValue: true,
- htmlMonitorRef: undefined,
- htmlMonitorSliderRef: undefined,
- htmlMonitorLabelRef: undefined,
- cssColor: '#0AA015',
- onChange: async (value) => {
- this.updateWidget(this.monitorCPUElement);
- await this.updateServer({
- switchCPU: value,
- });
- },
- }
+ value: void 0
});
- Object.defineProperty(this, "monitorRAMElement", {
+ Object.defineProperty(this, "monitorWidthId", {
enumerable: true,
configurable: true,
writable: true,
- value: {
- id: 'Crystools.switchRAM',
- name: this.menuPrefix + ' [monitor] RAM Used',
- type: 'boolean',
- label: 'RAM',
- symbol: '%',
- defaultValue: true,
- htmlMonitorRef: undefined,
- htmlMonitorSliderRef: undefined,
- htmlMonitorLabelRef: undefined,
- cssColor: '#07630D',
- onChange: async (value) => {
- this.updateWidget(this.monitorRAMElement);
- await this.updateServer({
- switchRAM: value,
- });
- },
- }
+ value: 'Crystools.MonitorWidth'
});
- Object.defineProperty(this, "monitorHDDElement", {
+ Object.defineProperty(this, "monitorWidth", {
enumerable: true,
configurable: true,
writable: true,
- value: {
- id: 'Crystools.switchHDD',
- name: this.menuPrefix + ' [monitor] HDD Used',
- type: 'boolean',
- label: 'HDD',
- symbol: '%',
- tooltip: 'See Partition to show (HDD)',
- defaultValue: true,
- htmlMonitorRef: undefined,
- htmlMonitorSliderRef: undefined,
- htmlMonitorLabelRef: undefined,
- cssColor: '#730F92',
- onChange: async (value) => {
- this.updateWidget(this.monitorHDDElement);
- await this.updateServer({
- switchHDD: value,
- });
- },
- }
+ value: 60
});
- Object.defineProperty(this, "createSettings", {
+ Object.defineProperty(this, "monitorHeightId", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: 'Crystools.MonitorHeight'
+ });
+ Object.defineProperty(this, "monitorHeight", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: 30
+ });
+ Object.defineProperty(this, "createSettingsRate", {
enumerable: true,
configurable: true,
writable: true,
value: () => {
- app.ui.settings.addSetting(this.monitorCPUElement);
- app.ui.settings.addSetting(this.monitorRAMElement);
- app.ui.settings.addSetting(this.monitorHDDElement);
- app.ui.settings.addSetting({
- id: this.idInputRate,
- name: this.menuPrefix + '[monitor] Refresh rate',
+ this.settingsRate = {
+ id: 'Crystools.RefreshRate',
+ name: 'Refresh per second',
+ category: ['Crystools', this.menuPrefix + ' Configuration', 'refresh'],
tooltip: 'This is the time (in seconds) between each update of the monitors, 0 means no refresh',
type: 'slider',
attrs: {
min: 0,
- max: 5,
- step: 0.25,
+ max: 2,
+ step: .25,
},
- defaultValue: this.defaultRate,
+ defaultValue: .5,
onChange: async (value) => {
let valueNumber;
try {
@@ -171,143 +150,408 @@ class CrystoolsMonitor {
return;
}
try {
- await this.updateServer({
- rate: valueNumber,
- });
+ await this.updateServer({ rate: valueNumber });
}
catch (error) {
console.error(error);
return;
}
+ const data = {
+ cpu_utilization: 0,
+ device: 'cpu',
+ gpus: [
+ {
+ gpu_utilization: 0,
+ gpu_temperature: 0,
+ vram_total: 0,
+ vram_used: 0,
+ vram_used_percent: 0,
+ },
+ ],
+ hdd_total: 0,
+ hdd_used: 0,
+ hdd_used_percent: 0,
+ ram_total: 0,
+ ram_used: 0,
+ ram_used_percent: 0,
+ };
if (valueNumber === 0) {
- this.updateAllMonitors({
- cpu_utilization: 0,
- device: 'cpu',
- gpus: [
- {
- gpu_utilization: 0,
- gpu_temperature: 0,
- vram_total: 0,
- vram_used: 0,
- vram_used_percent: 0,
- },
- ],
- hdd_total: 0,
- hdd_used: 0,
- hdd_used_percent: 0,
- ram_total: 0,
- ram_used: 0,
- ram_used_percent: 0,
- });
+ this.monitorUI.updateDisplay(data);
}
- this.updateAllAnimationDuration(valueNumber);
+ this.monitorUI?.updateAllAnimationDuration(valueNumber);
},
- });
- void this.getHDDsFromServer().then((data) => {
- const which = app.ui.settings.getSettingValue(this.idWhichHDD, this.defaultWhichHDD);
- app.ui.settings.addSetting({
- id: this.idWhichHDD,
- name: this.menuPrefix + '[monitor] Partition to show',
- type: 'combo',
- defaultValue: this.defaultWhichHDD,
- options: (value) => data.map((m) => ({
+ };
+ }
+ });
+ Object.defineProperty(this, "createSettingsCPU", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: () => {
+ this.monitorCPUElement = {
+ id: 'Crystools.ShowCpu',
+ name: 'CPU Usage',
+ category: ['Crystools', this.menuPrefix + ' Hardware', 'Cpu'],
+ type: 'boolean',
+ label: 'CPU',
+ symbol: '%',
+ defaultValue: true,
+ htmlMonitorRef: undefined,
+ htmlMonitorSliderRef: undefined,
+ htmlMonitorLabelRef: undefined,
+ cssColor: Colors.CPU,
+ onChange: async (value) => {
+ this.updateWidget(this.monitorCPUElement);
+ await this.updateServer({ switchCPU: value });
+ },
+ };
+ }
+ });
+ Object.defineProperty(this, "createSettingsRAM", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: () => {
+ this.monitorRAMElement = {
+ id: 'Crystools.ShowRam',
+ name: 'RAM Used',
+ category: ['Crystools', this.menuPrefix + ' Hardware', 'Ram'],
+ type: 'boolean',
+ label: 'RAM',
+ symbol: '%',
+ defaultValue: true,
+ htmlMonitorRef: undefined,
+ htmlMonitorSliderRef: undefined,
+ htmlMonitorLabelRef: undefined,
+ cssColor: Colors.RAM,
+ onChange: async (value) => {
+ this.updateWidget(this.monitorRAMElement);
+ await this.updateServer({ switchRAM: value });
+ },
+ };
+ }
+ });
+ Object.defineProperty(this, "createSettingsGPUUsage", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: (name, index, moreThanOneGPU) => {
+ if (name === undefined || index === undefined) {
+ console.warn('getGPUsFromServer: name or index undefined', name, index);
+ return;
+ }
+ let label = 'GPU ';
+ label += moreThanOneGPU ? index : '';
+ const monitorGPUNElement = {
+ id: 'Crystools.ShowGpuUsage' + convertNumberToPascalCase(index),
+ name: ' Usage',
+ category: ['Crystools', `${this.menuPrefix} Show GPU [${index}] ${name}`, 'Usage'],
+ type: 'boolean',
+ label,
+ symbol: '%',
+ monitorTitle: `${index}: ${name}`,
+ defaultValue: true,
+ htmlMonitorRef: undefined,
+ htmlMonitorSliderRef: undefined,
+ htmlMonitorLabelRef: undefined,
+ cssColor: Colors.GPU,
+ onChange: async (value) => {
+ this.updateWidget(monitorGPUNElement);
+ void await this.updateServerGPU(index, { utilization: value });
+ },
+ };
+ this.monitorGPUSettings[index] = monitorGPUNElement;
+ app.ui.settings.addSetting(this.monitorGPUSettings[index]);
+ this.monitorUI.createDOMGPUMonitor(this.monitorGPUSettings[index]);
+ }
+ });
+ Object.defineProperty(this, "createSettingsGPUVRAM", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: (name, index, moreThanOneGPU) => {
+ if (name === undefined || index === undefined) {
+ console.warn('getGPUsFromServer: name or index undefined', name, index);
+ return;
+ }
+ let label = 'VRAM ';
+ label += moreThanOneGPU ? index : '';
+ const monitorVRAMNElement = {
+ id: 'Crystools.ShowGpuVram' + convertNumberToPascalCase(index),
+ name: 'VRAM',
+ category: ['Crystools', `${this.menuPrefix} Show GPU [${index}] ${name}`, 'VRAM'],
+ type: 'boolean',
+ label: label,
+ symbol: '%',
+ monitorTitle: `${index}: ${name}`,
+ defaultValue: true,
+ htmlMonitorRef: undefined,
+ htmlMonitorSliderRef: undefined,
+ htmlMonitorLabelRef: undefined,
+ cssColor: Colors.VRAM,
+ onChange: async (value) => {
+ this.updateWidget(monitorVRAMNElement);
+ void await this.updateServerGPU(index, { vram: value });
+ },
+ };
+ this.monitorVRAMSettings[index] = monitorVRAMNElement;
+ app.ui.settings.addSetting(this.monitorVRAMSettings[index]);
+ this.monitorUI.createDOMGPUMonitor(this.monitorVRAMSettings[index]);
+ }
+ });
+ Object.defineProperty(this, "createSettingsGPUTemp", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: (name, index, moreThanOneGPU) => {
+ if (name === undefined || index === undefined) {
+ console.warn('getGPUsFromServer: name or index undefined', name, index);
+ return;
+ }
+ let label = 'Temp ';
+ label += moreThanOneGPU ? index : '';
+ const monitorTemperatureNElement = {
+ id: 'Crystools.ShowGpuTemperature' + convertNumberToPascalCase(index),
+ name: 'Temperature',
+ category: ['Crystools', `${this.menuPrefix} Show GPU [${index}] ${name}`, 'Temperature'],
+ type: 'boolean',
+ label: label,
+ symbol: '°',
+ monitorTitle: `${index}: ${name}`,
+ defaultValue: true,
+ htmlMonitorRef: undefined,
+ htmlMonitorSliderRef: undefined,
+ htmlMonitorLabelRef: undefined,
+ cssColor: Colors.TEMP_START,
+ cssColorFinal: Colors.TEMP_END,
+ onChange: async (value) => {
+ this.updateWidget(monitorTemperatureNElement);
+ void await this.updateServerGPU(index, { temperature: value });
+ },
+ };
+ this.monitorTemperatureSettings[index] = monitorTemperatureNElement;
+ app.ui.settings.addSetting(this.monitorTemperatureSettings[index]);
+ this.monitorUI.createDOMGPUMonitor(this.monitorTemperatureSettings[index]);
+ }
+ });
+ Object.defineProperty(this, "createSettingsHDD", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: () => {
+ this.monitorHDDElement = {
+ id: 'Crystools.ShowHdd',
+ name: 'Show HDD Used',
+ category: ['Crystools', this.menuPrefix + ' Show Hard Disk', 'Show'],
+ type: 'boolean',
+ label: 'HDD',
+ symbol: '%',
+ defaultValue: false,
+ htmlMonitorRef: undefined,
+ htmlMonitorSliderRef: undefined,
+ htmlMonitorLabelRef: undefined,
+ cssColor: Colors.DISK,
+ onChange: async (value) => {
+ this.updateWidget(this.monitorHDDElement);
+ await this.updateServer({ switchHDD: value });
+ },
+ };
+ this.settingsHDD = {
+ id: 'Crystools.WhichHdd',
+ name: 'Partition to show',
+ category: ['Crystools', this.menuPrefix + ' Show Hard Disk', 'Which'],
+ type: 'combo',
+ defaultValue: '/',
+ data: [],
+ options: (value) => {
+ const which = app.ui.settings.getSettingValue(this.settingsHDD.id, this.settingsHDD.defaultValue);
+ return this.settingsHDD.data.map((m) => ({
value: m,
text: m,
selected: !value ? m === which : m === value,
- })),
- onChange: async (value) => {
- await this.updateServer({
- whichHDD: value,
- });
- },
- });
+ }));
+ },
+ onChange: async (value) => {
+ await this.updateServer({ whichHDD: value });
+ },
+ };
+ }
+ });
+ Object.defineProperty(this, "createSettingsMonitorWidth", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: () => {
+ this.settingsMonitorWidth = {
+ id: this.monitorWidthId,
+ name: 'Pixel Width',
+ category: ['Crystools', this.menuPrefix + ' Configuration', 'width'],
+ tooltip: 'The width of the monitor in pixels on the UI (only on floating UI)',
+ type: 'slider',
+ attrs: {
+ min: 60,
+ max: 100,
+ step: 1,
+ },
+ defaultValue: this.monitorWidth,
+ onChange: (value) => {
+ let valueNumber;
+ try {
+ valueNumber = parseInt(value);
+ if (isNaN(valueNumber)) {
+ throw new Error('invalid value');
+ }
+ }
+ catch (error) {
+ console.error(error);
+ return;
+ }
+ const h = app.ui.settings.getSettingValue(this.monitorHeightId, this.monitorHeight);
+ this.monitorUI?.updateMonitorSize(valueNumber, h);
+ },
+ };
+ }
+ });
+ Object.defineProperty(this, "createSettingsMonitorHeight", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: () => {
+ this.settingsMonitorHeight = {
+ id: this.monitorHeightId,
+ name: 'Pixel Height',
+ category: ['Crystools', this.menuPrefix + ' Configuration', 'height'],
+ tooltip: 'The height of the monitor in pixels on the UI (only on floating UI)',
+ type: 'slider',
+ attrs: {
+ min: 16,
+ max: 50,
+ step: 1,
+ },
+ defaultValue: this.monitorHeight,
+ onChange: async (value) => {
+ let valueNumber;
+ try {
+ valueNumber = parseInt(value);
+ if (isNaN(valueNumber)) {
+ throw new Error('invalid value');
+ }
+ }
+ catch (error) {
+ console.error(error);
+ return;
+ }
+ const w = await app.ui.settings.getSettingValue(this.monitorWidthId, this.monitorWidth);
+ this.monitorUI?.updateMonitorSize(w, valueNumber);
+ },
+ };
+ }
+ });
+ Object.defineProperty(this, "createSettings", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: () => {
+ app.ui.settings.addSetting(this.settingsRate);
+ app.ui.settings.addSetting(this.settingsMonitorHeight);
+ app.ui.settings.addSetting(this.settingsMonitorWidth);
+ app.ui.settings.addSetting(this.monitorRAMElement);
+ app.ui.settings.addSetting(this.monitorCPUElement);
+ void this.getHDDsFromServer().then((data) => {
+ this.settingsHDD.data = data;
+ app.ui.settings.addSetting(this.settingsHDD);
});
+ app.ui.settings.addSetting(this.monitorHDDElement);
void this.getGPUsFromServer().then((gpus) => {
let moreThanOneGPU = false;
if (gpus.length > 1) {
moreThanOneGPU = true;
}
gpus?.forEach(({ name, index }) => {
- if (name === undefined || index === undefined) {
- console.warn('getGPUsFromServer: name or index undefined', name, index);
- return;
- }
- let label = 'GPU';
- let labelVRAM = 'VRAM';
- let labelTemperature = 'Temp';
- if (moreThanOneGPU) {
- label = 'GPU ' + index;
- labelVRAM = 'VRAM' + index;
- labelTemperature = 'Temp ' + index;
- }
- const monitorGPUNElement = {
- id: 'Crystools.switchGPU' + index,
- name: this.menuPrefix + `[menu] Display GPU\r\n[${index}] ${name}`,
- type: 'boolean',
- label,
- symbol: '%',
- title: `${index}: ${name}`,
- defaultValue: true,
- htmlMonitorRef: undefined,
- htmlMonitorSliderRef: undefined,
- htmlMonitorLabelRef: undefined,
- cssColor: '#0C86F4',
- onChange: async (value) => {
- this.updateWidget(monitorGPUNElement);
- void await this.updateServerGPU(index, {
- utilization: value
- });
- },
- };
- const monitorVRAMNElement = {
- id: 'Crystools.switchVRAM' + index,
- name: this.menuPrefix + `[menu] Display GPU VRAM\r\n[${index}] ${name}`,
- type: 'boolean',
- label: labelVRAM,
- symbol: '%',
- title: `${index}: ${name}`,
- defaultValue: true,
- htmlMonitorRef: undefined,
- htmlMonitorSliderRef: undefined,
- htmlMonitorLabelRef: undefined,
- cssColor: '#176EC7',
- onChange: async (value) => {
- this.updateWidget(monitorVRAMNElement);
- void await this.updateServerGPU(index, {
- vram: value
- });
- },
- };
- const monitorTemperatureNElement = {
- id: 'Crystools.switchTemperature' + index,
- name: this.menuPrefix + `[menu] Display GPU Temperature\r\n[${index}] ${name}`,
- type: 'boolean',
- label: labelTemperature,
- symbol: '°',
- title: `${index}: ${name}`,
- defaultValue: true,
- htmlMonitorRef: undefined,
- htmlMonitorSliderRef: undefined,
- htmlMonitorLabelRef: undefined,
- cssColor: '#00ff00',
- cssColorFinal: '#ff0000',
- onChange: async (value) => {
- this.updateWidget(monitorTemperatureNElement);
- void await this.updateServerGPU(index, {
- temperature: value
- });
- },
- };
- this.monitorGPUSettings[index] = monitorGPUNElement;
- this.monitorVRAMSettings[index] = monitorVRAMNElement;
- this.monitorTemperatureSettings[index] = monitorTemperatureNElement;
- app.ui.settings.addSetting(this.monitorGPUSettings[index]);
- app.ui.settings.addSetting(this.monitorVRAMSettings[index]);
- app.ui.settings.addSetting(this.monitorTemperatureSettings[index]);
+ this.createSettingsGPUTemp(name, index, moreThanOneGPU);
+ this.createSettingsGPUVRAM(name, index, moreThanOneGPU);
+ this.createSettingsGPUUsage(name, index, moreThanOneGPU);
});
+ this.finishedLoad();
});
}
});
+ Object.defineProperty(this, "finishedLoad", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: () => {
+ this.monitorUI.orderMonitors();
+ this.updateAllWidget();
+ this.moveMonitor(this.menuDisplayOption);
+ const w = app.ui.settings.getSettingValue(this.monitorWidthId, this.monitorWidth);
+ const h = app.ui.settings.getSettingValue(this.monitorHeightId, this.monitorHeight);
+ this.monitorUI.updateMonitorSize(w, h);
+ }
+ });
+ Object.defineProperty(this, "updateDisplay", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: (value) => {
+ if (value !== this.menuDisplayOption) {
+ this.menuDisplayOption = value;
+ this.moveMonitor(this.menuDisplayOption);
+ }
+ }
+ });
+ Object.defineProperty(this, "moveMonitor", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: (position) => {
+ let parentElement;
+ switch (position) {
+ case MenuDisplayOptions.Disabled:
+ parentElement = document.getElementById('queue-button');
+ if (parentElement && this.monitorUI.rootElement) {
+ parentElement.insertAdjacentElement('afterend', this.crystoolsButtonGroup.element);
+ }
+ else {
+ console.error('Crystools: parentElement to move monitors not found!', parentElement);
+ }
+ break;
+ case MenuDisplayOptions.Top:
+ case MenuDisplayOptions.Bottom:
+ app.menu?.settingsGroup.element.before(this.crystoolsButtonGroup.element);
+ }
+ }
+ });
+ Object.defineProperty(this, "updateAllWidget", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: () => {
+ this.updateWidget(this.monitorCPUElement);
+ this.updateWidget(this.monitorRAMElement);
+ this.updateWidget(this.monitorHDDElement);
+ this.monitorGPUSettings.forEach((monitorSettings) => {
+ monitorSettings && this.updateWidget(monitorSettings);
+ });
+ this.monitorVRAMSettings.forEach((monitorSettings) => {
+ monitorSettings && this.updateWidget(monitorSettings);
+ });
+ this.monitorTemperatureSettings.forEach((monitorSettings) => {
+ monitorSettings && this.updateWidget(monitorSettings);
+ });
+ }
+ });
+ Object.defineProperty(this, "updateWidget", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: (monitorSettings) => {
+ if (this.monitorUI) {
+ const value = app.ui.settings.getSettingValue(monitorSettings.id, monitorSettings.defaultValue);
+ this.monitorUI.showMonitor(monitorSettings, value);
+ }
+ }
+ });
Object.defineProperty(this, "updateServer", {
enumerable: true,
configurable: true,
@@ -371,183 +615,31 @@ class CrystoolsMonitor {
throw new Error(resp.statusText);
}
});
- Object.defineProperty(this, "updateAllWidget", {
+ Object.defineProperty(this, "setup", {
enumerable: true,
configurable: true,
writable: true,
value: () => {
- this.updateWidget(this.monitorCPUElement);
- this.updateWidget(this.monitorRAMElement);
- this.updateWidget(this.monitorHDDElement);
- this.monitorGPUSettings.forEach((monitorSettings) => {
- monitorSettings && this.updateWidget(monitorSettings);
- });
- this.monitorVRAMSettings.forEach((monitorSettings) => {
- monitorSettings && this.updateWidget(monitorSettings);
- });
- this.monitorTemperatureSettings.forEach((monitorSettings) => {
- monitorSettings && this.updateWidget(monitorSettings);
- });
- }
- });
- Object.defineProperty(this, "updateWidget", {
- enumerable: true,
- configurable: true,
- writable: true,
- value: (monitorSettings) => {
- const value = app.ui.settings.getSettingValue(monitorSettings.id, monitorSettings.defaultValue);
- if (monitorSettings.htmlMonitorRef) {
- monitorSettings.htmlMonitorRef.style.display = value ? 'flex' : 'none';
- }
- }
- });
- Object.defineProperty(this, "updateAllMonitors", {
- enumerable: true,
- configurable: true,
- writable: true,
- value: (data) => {
- this.updateMonitor(this.monitorCPUElement, data.cpu_utilization);
- this.updateMonitor(this.monitorRAMElement, data.ram_used_percent);
- this.updateMonitor(this.monitorHDDElement, data.hdd_used_percent);
- if (data.gpus === undefined || data.gpus.length === 0) {
- console.warn('UpdateAllMonitors: no GPU data');
+ if (this.monitorUI) {
return;
}
- this.monitorGPUSettings.forEach((monitorSettings, index) => {
- if (data.gpus[index]) {
- const gpu = data.gpus[index];
- if (gpu === undefined) {
- return;
- }
- this.updateMonitor(monitorSettings, gpu.gpu_utilization);
- }
- else {
- }
+ this.createSettingsRate();
+ this.createSettingsMonitorHeight();
+ this.createSettingsMonitorWidth();
+ this.createSettingsCPU();
+ this.createSettingsRAM();
+ this.createSettingsHDD();
+ this.createSettings();
+ const currentRate = parseFloat(app.ui.settings.getSettingValue(this.settingsRate.id, this.settingsRate.defaultValue));
+ this.menuDisplayOption = app.ui.settings.getSettingValue(ComfyKeyMenuDisplayOption, MenuDisplayOptions.Disabled);
+ app.ui.settings.addEventListener(`${ComfyKeyMenuDisplayOption}.change`, (e) => {
+ this.updateDisplay(e.detail.value);
});
- this.monitorVRAMSettings.forEach((monitorSettings, index) => {
- if (data.gpus[index]) {
- const gpu = data.gpus[index];
- if (gpu === undefined) {
- return;
- }
- this.updateMonitor(monitorSettings, gpu.vram_used_percent);
- }
- else {
- }
- });
- this.monitorTemperatureSettings.forEach((monitorSettings, index) => {
- if (data.gpus[index]) {
- const gpu = data.gpus[index];
- if (gpu === undefined) {
- return;
- }
- this.updateMonitor(monitorSettings, gpu.gpu_temperature);
- if (monitorSettings.cssColorFinal && monitorSettings.htmlMonitorSliderRef) {
- monitorSettings.htmlMonitorSliderRef.style.backgroundColor =
- `color-mix(in srgb, ${monitorSettings.cssColorFinal} ${gpu.gpu_temperature}%, ${monitorSettings.cssColor})`;
- }
- }
- else {
- }
- });
- }
- });
- Object.defineProperty(this, "updateMonitor", {
- enumerable: true,
- configurable: true,
- writable: true,
- value: (monitorSettings, percent) => {
- if (!(monitorSettings.htmlMonitorSliderRef && monitorSettings.htmlMonitorLabelRef)) {
- return;
- }
- monitorSettings.htmlMonitorLabelRef.innerHTML = `${Math.floor(percent)}${monitorSettings.symbol}`;
- monitorSettings.htmlMonitorSliderRef.style.width = `${Math.floor(percent)}%`;
- }
- });
- Object.defineProperty(this, "updateAllAnimationDuration", {
- enumerable: true,
- configurable: true,
- writable: true,
- value: (value) => {
- this.updatedAnimationDuration(this.monitorCPUElement, value);
- this.updatedAnimationDuration(this.monitorRAMElement, value);
- this.updatedAnimationDuration(this.monitorHDDElement, value);
- this.monitorGPUSettings.forEach((monitorSettings) => {
- monitorSettings && this.updatedAnimationDuration(monitorSettings, value);
- });
- this.monitorVRAMSettings.forEach((monitorSettings) => {
- monitorSettings && this.updatedAnimationDuration(monitorSettings, value);
- });
- this.monitorTemperatureSettings.forEach((monitorSettings) => {
- monitorSettings && this.updatedAnimationDuration(monitorSettings, value);
- });
- }
- });
- Object.defineProperty(this, "updatedAnimationDuration", {
- enumerable: true,
- configurable: true,
- writable: true,
- value: (monitorSettings, value) => {
- const slider = monitorSettings.htmlMonitorSliderRef;
- if (!slider) {
- return;
- }
- slider.style.transition = `width ${value.toFixed(1)}s`;
- }
- });
- Object.defineProperty(this, "createMonitor", {
- enumerable: true,
- configurable: true,
- writable: true,
- value: (monitorSettings) => {
- if (!monitorSettings) {
- return document.createElement('div');
- }
- const htmlMain = document.createElement('div');
- htmlMain.setAttribute('id', monitorSettings.id);
- htmlMain.style.margin = '2px 10px';
- htmlMain.style.height = '12px';
- htmlMain.style.position = 'relative';
- htmlMain.style.display = 'flex';
- htmlMain.style.alignItems = 'center';
- htmlMain.style.flexDirection = 'row';
- monitorSettings.htmlMonitorRef = htmlMain;
- if (monitorSettings.title) {
- htmlMain.title = monitorSettings.title;
- }
- const htmlMonitorText = document.createElement('div');
- htmlMonitorText.style.width = '35px';
- htmlMonitorText.style.fontSize = '10px';
- htmlMonitorText.innerHTML = monitorSettings.label;
- htmlMain.append(htmlMonitorText);
- const htmlMonitorContent = document.createElement('div');
- htmlMonitorContent.style.height = '100%';
- htmlMonitorContent.style.flexGrow = '1';
- htmlMonitorContent.style.position = 'relative';
- htmlMonitorContent.style.backgroundColor = 'var(--bg-color)';
- htmlMain.append(htmlMonitorContent);
- const htmlMonitorSlider = document.createElement('div');
- htmlMonitorSlider.style.position = 'absolute';
- htmlMonitorSlider.style.height = '100%';
- htmlMonitorSlider.style.width = '0';
- if (monitorSettings.cssColorFinal) {
- htmlMonitorSlider.style.backgroundColor =
- `color-mix(in srgb, ${monitorSettings.cssColorFinal} 0%, ${monitorSettings.cssColor})`;
- }
- else {
- htmlMonitorSlider.style.backgroundColor = monitorSettings.cssColor;
- }
- monitorSettings.htmlMonitorSliderRef = htmlMonitorSlider;
- htmlMonitorContent.append(htmlMonitorSlider);
- const htmlMonitorLabel = document.createElement('div');
- htmlMonitorLabel.style.position = 'relative';
- htmlMonitorLabel.style.width = '100%';
- htmlMonitorLabel.style.color = 'var(--drag-text)';
- htmlMonitorLabel.style.fontSize = '10px';
- htmlMonitorLabel.innerHTML = '0%';
- monitorSettings.htmlMonitorLabelRef = htmlMonitorLabel;
- htmlMonitorContent.append(htmlMonitorLabel);
- return monitorSettings.htmlMonitorRef;
+ this.crystoolsButtonGroup = new ComfyButtonGroup();
+ app.menu?.settingsGroup.element.before(this.crystoolsButtonGroup.element);
+ this.monitorUI = new MonitorUI(this.crystoolsButtonGroup.element, this.monitorCPUElement, this.monitorRAMElement, this.monitorHDDElement, this.monitorGPUSettings, this.monitorVRAMSettings, this.monitorTemperatureSettings, currentRate);
+ this.updateDisplay(this.menuDisplayOption);
+ this.registerListeners();
}
});
Object.defineProperty(this, "registerListeners", {
@@ -559,47 +651,14 @@ class CrystoolsMonitor {
if (event?.detail === undefined) {
return;
}
- this.updateAllMonitors(event.detail);
+ this.monitorUI.updateDisplay(event.detail);
}, false);
}
});
- this.createSettings();
- }
- setup() {
- const parentElement = document.getElementById('queue-button');
- let ctoolsRoot = document.getElementById(this.htmlIdCrystoolsRoot);
- if (!ctoolsRoot) {
- ctoolsRoot = document.createElement('div');
- ctoolsRoot.setAttribute('id', this.htmlIdCrystoolsRoot);
- ctoolsRoot.style.display = 'flex';
- ctoolsRoot.style.width = '100%';
- ctoolsRoot.style.flexDirection = 'column';
- parentElement?.insertAdjacentElement('afterend', ctoolsRoot);
- }
- const htmlContainer = document.createElement('div');
- htmlContainer.setAttribute('id', this.htmlIdCrystoolsMonitorContainer);
- htmlContainer.style.width = '100%';
- htmlContainer.style.cursor = 'crosshair';
- htmlContainer.style.order = '3';
- htmlContainer.style.margin = '4px 0';
- ctoolsRoot.append(htmlContainer);
- htmlContainer.append(this.createMonitor(this.monitorCPUElement));
- htmlContainer.append(this.createMonitor(this.monitorRAMElement));
- this.monitorGPUSettings.forEach((_monitorSettings, index) => {
- this.monitorGPUSettings[index] && htmlContainer.append(this.createMonitor(this.monitorGPUSettings[index]));
- this.monitorVRAMSettings[index] && htmlContainer.append(this.createMonitor(this.monitorVRAMSettings[index]));
- this.monitorTemperatureSettings[index] &&
- htmlContainer.append(this.createMonitor(this.monitorTemperatureSettings[index]));
- });
- htmlContainer.append(this.createMonitor(this.monitorHDDElement));
- const currentRate = parseFloat(app.ui.settings.getSettingValue(this.idInputRate, this.defaultRate));
- this.updateAllAnimationDuration(currentRate);
- this.updateAllWidget();
- this.registerListeners();
}
}
const crystoolsMonitor = new CrystoolsMonitor();
app.registerExtension({
name: crystoolsMonitor.idExtensionName,
- setup: crystoolsMonitor.setup.bind(crystoolsMonitor),
+ setup: crystoolsMonitor.setup,
});
diff --git a/web/monitor.ts b/web/monitor.ts
index 09f70a2..7b4c4e1 100644
--- a/web/monitor.ts
+++ b/web/monitor.ts
@@ -1,103 +1,50 @@
-import { app } from '/scripts/app.js';
-import { api } from '/scripts/api.js';
+import { app, api, ComfyButtonGroup } from './comfy/index.js';
import { commonPrefix } from './common.js';
+import { MonitorUI } from './monitorUI.js';
+import { Colors } from './styles.js';
+import { convertNumberToPascalCase } from './utils.js';
+import { ComfyKeyMenuDisplayOption, MenuDisplayOptions } from './progressBarUIBase.js';
class CrystoolsMonitor {
- idExtensionName = 'Crystools.monitor';
- menuPrefix = commonPrefix;
- htmlIdCrystoolsRoot = 'crystools-root';
- htmlIdCrystoolsMonitorContainer = 'crystools-monitor-container';
+ readonly idExtensionName = 'Crystools.monitor';
+ private readonly menuPrefix = commonPrefix;
+ private menuDisplayOption: MenuDisplayOptions = MenuDisplayOptions.Disabled;
+ private crystoolsButtonGroup: ComfyButtonGroup = null;
- idInputRate = 'Crystools.inputRate';
- defaultRate = .5;
- idWhichHDD = 'Crystools.whichHDD';
- defaultWhichHDD = '/';
- monitorGPUSettings: TMonitorSettings[] = [];
- monitorVRAMSettings: TMonitorSettings[] = [];
- monitorTemperatureSettings: TMonitorSettings[] = [];
+ private settingsRate: TMonitorSettings;
+ private settingsMonitorHeight: TMonitorSettings;
+ private settingsMonitorWidth: TMonitorSettings;
+ private monitorCPUElement: TMonitorSettings;
+ private monitorRAMElement: TMonitorSettings;
+ private monitorHDDElement: TMonitorSettings;
+ private settingsHDD: TMonitorSettings;
+ private monitorGPUSettings: TMonitorSettings[] = [];
+ private monitorVRAMSettings: TMonitorSettings[] = [];
+ private monitorTemperatureSettings: TMonitorSettings[] = [];
- // CPU Variables
- monitorCPUElement: TMonitorSettings = {
- id: 'Crystools.switchCPU',
- name: this.menuPrefix + ' [monitor] CPU Usage',
- type: 'boolean',
- label: 'CPU',
- symbol: '%',
- defaultValue: true,
- htmlMonitorRef: undefined,
- htmlMonitorSliderRef: undefined,
- htmlMonitorLabelRef: undefined,
- cssColor: '#0AA015',
- onChange: async(value: boolean) => {
- this.updateWidget(this.monitorCPUElement);
- await this.updateServer({
- switchCPU: value,
- });
- },
- };
+ private monitorUI: MonitorUI;
- // RAM Variables
- monitorRAMElement: TMonitorSettings = {
- id: 'Crystools.switchRAM',
- name: this.menuPrefix + ' [monitor] RAM Used',
- type: 'boolean',
- label: 'RAM',
- symbol: '%',
- defaultValue: true,
- htmlMonitorRef: undefined,
- htmlMonitorSliderRef: undefined,
- htmlMonitorLabelRef: undefined,
- cssColor: '#07630D',
- onChange: async(value: boolean) => {
- this.updateWidget(this.monitorRAMElement);
- await this.updateServer({
- switchRAM: value,
- });
- },
- };
+ private readonly monitorWidthId = 'Crystools.MonitorWidth';
+ private readonly monitorWidth = 60;
+ private readonly monitorHeightId = 'Crystools.MonitorHeight';
+ private readonly monitorHeight = 30;
- // HDD Variables
- monitorHDDElement: TMonitorSettings = {
- id: 'Crystools.switchHDD',
- name: this.menuPrefix + ' [monitor] HDD Used',
- type: 'boolean',
- label: 'HDD',
- symbol: '%',
- tooltip: 'See Partition to show (HDD)',
- defaultValue: true,
- htmlMonitorRef: undefined,
- htmlMonitorSliderRef: undefined,
- htmlMonitorLabelRef: undefined,
- cssColor: '#730F92',
- onChange: async(value: boolean) => {
- this.updateWidget(this.monitorHDDElement);
- await this.updateServer({
- switchHDD: value,
- });
- },
- };
-
- constructor() {
- this.createSettings();
- }
-
- createSettings = (): void => {
- app.ui.settings.addSetting(this.monitorCPUElement);
- app.ui.settings.addSetting(this.monitorRAMElement);
- app.ui.settings.addSetting(this.monitorHDDElement);
-
- app.ui.settings.addSetting({
- id: this.idInputRate,
- name: this.menuPrefix + '[monitor] Refresh rate',
+ createSettingsRate = (): void => {
+ this.settingsRate = {
+ id: 'Crystools.RefreshRate',
+ name: 'Refresh per second',
+ category: ['Crystools', this.menuPrefix + ' Configuration', 'refresh'],
tooltip: 'This is the time (in seconds) between each update of the monitors, 0 means no refresh',
type: 'slider',
attrs: {
min: 0,
- max: 5,
- step: 0.25,
+ max: 2,
+ step: .25,
},
- defaultValue: this.defaultRate,
- onChange: async(value: string) => {
+ defaultValue: .5,
+
+ // @ts-ignore
+ onChange: async(value: string): Promise => {
let valueNumber: number;
try {
@@ -110,62 +57,313 @@ class CrystoolsMonitor {
return;
}
try {
- await this.updateServer({
- rate: valueNumber,
- });
+ await this.updateServer({rate: valueNumber});
} catch (error) {
console.error(error);
return;
}
- if (valueNumber === 0) {
- this.updateAllMonitors({
- cpu_utilization: 0,
- device: 'cpu',
+ const data = {
+ cpu_utilization: 0,
+ device: 'cpu',
- gpus: [
- {
- gpu_utilization: 0,
- gpu_temperature: 0,
- vram_total: 0,
- vram_used: 0,
- vram_used_percent: 0,
- },
- ],
- hdd_total: 0,
- hdd_used: 0,
- hdd_used_percent: 0,
- ram_total: 0,
- ram_used: 0,
- ram_used_percent: 0,
- });
+ gpus: [
+ {
+ gpu_utilization: 0,
+ gpu_temperature: 0,
+ vram_total: 0,
+ vram_used: 0,
+ vram_used_percent: 0,
+ },
+ ],
+ hdd_total: 0,
+ hdd_used: 0,
+ hdd_used_percent: 0,
+ ram_total: 0,
+ ram_used: 0,
+ ram_used_percent: 0,
+ };
+ if (valueNumber === 0) {
+ this.monitorUI.updateDisplay(data);
}
- this.updateAllAnimationDuration(valueNumber);
+ this.monitorUI?.updateAllAnimationDuration(valueNumber);
},
- });
+ };
+ };
+
+ createSettingsCPU = (): void => {
+ // CPU Variables
+ this.monitorCPUElement = {
+ id: 'Crystools.ShowCpu',
+ name: 'CPU Usage',
+ category: ['Crystools', this.menuPrefix + ' Hardware', 'Cpu'],
+ type: 'boolean',
+ label: 'CPU',
+ symbol: '%',
+ defaultValue: true,
+ htmlMonitorRef: undefined,
+ htmlMonitorSliderRef: undefined,
+ htmlMonitorLabelRef: undefined,
+ cssColor: Colors.CPU,
+ // @ts-ignore
+ onChange: async(value: boolean): Promise => {
+ this.updateWidget(this.monitorCPUElement);
+ await this.updateServer({switchCPU: value});
+ },
+ };
+ };
+
+ createSettingsRAM = (): void => {
+ // RAM Variables
+ this.monitorRAMElement = {
+ id: 'Crystools.ShowRam',
+ name: 'RAM Used',
+ category: ['Crystools', this.menuPrefix + ' Hardware', 'Ram'],
+ type: 'boolean',
+ label: 'RAM',
+ symbol: '%',
+ defaultValue: true,
+ htmlMonitorRef: undefined,
+ htmlMonitorSliderRef: undefined,
+ htmlMonitorLabelRef: undefined,
+ cssColor: Colors.RAM,
+ // @ts-ignore
+ onChange: async(value: boolean): Promise => {
+ this.updateWidget(this.monitorRAMElement);
+ await this.updateServer({switchRAM: value});
+ },
+ };
+ };
+
+ createSettingsGPUUsage = (name: string, index: number, moreThanOneGPU: boolean): void => {
+ if (name === undefined || index === undefined) {
+ console.warn('getGPUsFromServer: name or index undefined', name, index);
+ return;
+ }
+
+ let label = 'GPU ';
+ label += moreThanOneGPU ? index : '';
+
+ const monitorGPUNElement: TMonitorSettings = {
+ id: 'Crystools.ShowGpuUsage' + convertNumberToPascalCase(index),
+ name: ' Usage',
+ category: ['Crystools', `${this.menuPrefix} Show GPU [${index}] ${name}`, 'Usage'],
+ type: 'boolean',
+ label,
+ symbol: '%',
+ monitorTitle: `${index}: ${name}`,
+ defaultValue: true,
+ htmlMonitorRef: undefined,
+ htmlMonitorSliderRef: undefined,
+ htmlMonitorLabelRef: undefined,
+ cssColor: Colors.GPU,
+ // @ts-ignore
+ onChange: async(value: boolean): Promise => {
+ this.updateWidget(monitorGPUNElement);
+ void await this.updateServerGPU(index, {utilization: value});
+ },
+ };
+
+ this.monitorGPUSettings[index] = monitorGPUNElement;
+ app.ui.settings.addSetting(this.monitorGPUSettings[index]);
+ this.monitorUI.createDOMGPUMonitor(this.monitorGPUSettings[index]);
+ };
+
+ createSettingsGPUVRAM = (name: string, index: number, moreThanOneGPU: boolean): void => {
+ if (name === undefined || index === undefined) {
+ console.warn('getGPUsFromServer: name or index undefined', name, index);
+ return;
+ }
+
+ let label = 'VRAM ';
+ label += moreThanOneGPU ? index : '';
+
+ // GPU VRAM Variables
+ const monitorVRAMNElement: TMonitorSettings = {
+ id: 'Crystools.ShowGpuVram' + convertNumberToPascalCase(index),
+ name: 'VRAM',
+ category: ['Crystools', `${this.menuPrefix} Show GPU [${index}] ${name}`, 'VRAM'],
+ type: 'boolean',
+ label: label,
+ symbol: '%',
+ monitorTitle: `${index}: ${name}`,
+ defaultValue: true,
+ htmlMonitorRef: undefined,
+ htmlMonitorSliderRef: undefined,
+ htmlMonitorLabelRef: undefined,
+ cssColor: Colors.VRAM,
+ // @ts-ignore
+ onChange: async(value: boolean): Promise => {
+ this.updateWidget(monitorVRAMNElement);
+ void await this.updateServerGPU(index, {vram: value});
+ },
+ };
+
+ this.monitorVRAMSettings[index] = monitorVRAMNElement;
+ app.ui.settings.addSetting(this.monitorVRAMSettings[index]);
+ this.monitorUI.createDOMGPUMonitor(this.monitorVRAMSettings[index]);
+ };
+
+ createSettingsGPUTemp = (name: string, index: number, moreThanOneGPU: boolean): void => {
+ if (name === undefined || index === undefined) {
+ console.warn('getGPUsFromServer: name or index undefined', name, index);
+ return;
+ }
+
+ let label = 'Temp ';
+ label += moreThanOneGPU ? index : '';
+
+ // GPU Temperature Variables
+ const monitorTemperatureNElement: TMonitorSettings = {
+ id: 'Crystools.ShowGpuTemperature' + convertNumberToPascalCase(index),
+ name: 'Temperature',
+ category: ['Crystools', `${this.menuPrefix} Show GPU [${index}] ${name}`, 'Temperature'],
+ type: 'boolean',
+ label: label,
+ symbol: '°',
+ monitorTitle: `${index}: ${name}`,
+ defaultValue: true,
+ htmlMonitorRef: undefined,
+ htmlMonitorSliderRef: undefined,
+ htmlMonitorLabelRef: undefined,
+ cssColor: Colors.TEMP_START,
+ cssColorFinal: Colors.TEMP_END,
+ // @ts-ignore
+ onChange: async(value: boolean): Promise => {
+ this.updateWidget(monitorTemperatureNElement);
+ void await this.updateServerGPU(index, {temperature: value});
+ },
+ };
+
+ this.monitorTemperatureSettings[index] = monitorTemperatureNElement;
+ app.ui.settings.addSetting(this.monitorTemperatureSettings[index]);
+ this.monitorUI.createDOMGPUMonitor(this.monitorTemperatureSettings[index]);
+ };
+
+ createSettingsHDD = (): void => {
+ // HDD Variables
+ this.monitorHDDElement = {
+ id: 'Crystools.ShowHdd',
+ name: 'Show HDD Used',
+ category: ['Crystools', this.menuPrefix + ' Show Hard Disk', 'Show'],
+ type: 'boolean',
+ label: 'HDD',
+ symbol: '%',
+ // tooltip: 'See Partition to show (HDD)',
+ defaultValue: false,
+ htmlMonitorRef: undefined,
+ htmlMonitorSliderRef: undefined,
+ htmlMonitorLabelRef: undefined,
+ cssColor: Colors.DISK,
+ // @ts-ignore
+ onChange: async(value: boolean): Promise => {
+ this.updateWidget(this.monitorHDDElement);
+ await this.updateServer({switchHDD: value});
+ },
+ };
+
+ this.settingsHDD = {
+ id: 'Crystools.WhichHdd',
+ name: 'Partition to show',
+ category: ['Crystools', this.menuPrefix + ' Show Hard Disk', 'Which'],
+ type: 'combo',
+ defaultValue: '/',
+ data: [],
+ // @ts-ignore bad definition from comfyUI: `options?: undefined;`??
+ options: (value: string): any => {
+ const which = app.ui.settings.getSettingValue(this.settingsHDD.id, this.settingsHDD.defaultValue);
+ return this.settingsHDD.data.map((m: any) => ({
+ value: m,
+ text: m,
+ selected: !value ? m === which : m === value,
+ }));
+ },
+ // @ts-ignore
+ onChange: async(value: string): Promise => {
+ await this.updateServer({whichHDD: value});
+ },
+ };
+ };
+
+ createSettingsMonitorWidth = (): void => {
+ this.settingsMonitorWidth = {
+ id: this.monitorWidthId,
+ name: 'Pixel Width',
+ category: ['Crystools', this.menuPrefix + ' Configuration', 'width'],
+ tooltip: 'The width of the monitor in pixels on the UI (only on floating UI)',
+ type: 'slider',
+ attrs: {
+ min: 60,
+ max: 100,
+ step: 1,
+ },
+ defaultValue: this.monitorWidth,
+ // @ts-ignore
+ onChange: (value: string): void => {
+ let valueNumber: number;
+
+ try {
+ valueNumber = parseInt(value);
+ if (isNaN(valueNumber)) {
+ throw new Error('invalid value');
+ }
+ } catch (error) {
+ console.error(error);
+ return;
+ }
+
+ const h = app.ui.settings.getSettingValue(this.monitorHeightId, this.monitorHeight);
+ this.monitorUI?.updateMonitorSize(valueNumber, h);
+ },
+ };
+ };
+
+ createSettingsMonitorHeight = (): void => {
+ this.settingsMonitorHeight = {
+ id: this.monitorHeightId,
+ name: 'Pixel Height',
+ category: ['Crystools', this.menuPrefix + ' Configuration', 'height'],
+ tooltip: 'The height of the monitor in pixels on the UI (only on floating UI)',
+ type: 'slider',
+ attrs: {
+ min: 16,
+ max: 50,
+ step: 1,
+ },
+ defaultValue: this.monitorHeight,
+ // @ts-ignore
+ onChange: async(value: string): void => {
+ let valueNumber: number;
+
+ try {
+ valueNumber = parseInt(value);
+ if (isNaN(valueNumber)) {
+ throw new Error('invalid value');
+ }
+ } catch (error) {
+ console.error(error);
+ return;
+ }
+
+ const w = await app.ui.settings.getSettingValue(this.monitorWidthId, this.monitorWidth);
+ this.monitorUI?.updateMonitorSize(w, valueNumber);
+ },
+ };
+ };
+
+ createSettings = (): void => {
+ app.ui.settings.addSetting(this.settingsRate);
+ app.ui.settings.addSetting(this.settingsMonitorHeight);
+ app.ui.settings.addSetting(this.settingsMonitorWidth);
+ app.ui.settings.addSetting(this.monitorRAMElement);
+ app.ui.settings.addSetting(this.monitorCPUElement);
void this.getHDDsFromServer().then((data: string[]): void => {
- const which = app.ui.settings.getSettingValue(this.idWhichHDD, this.defaultWhichHDD);
- app.ui.settings.addSetting({
- id: this.idWhichHDD,
- name: this.menuPrefix + '[monitor] Partition to show',
- type: 'combo',
- defaultValue: this.defaultWhichHDD,
- // @ts-ignore bad definition from comfyUI: `options?: undefined;`??
- options: (value: string) =>
- data.map((m) => ({
- value: m,
- text: m,
- selected: !value ? m === which : m === value,
- })),
- onChange: async(value: string) => {
- await this.updateServer({
- whichHDD: value,
- });
- },
- });
+ this.settingsHDD.data = data;
+ app.ui.settings.addSetting(this.settingsHDD);
});
+ app.ui.settings.addSetting(this.monitorHDDElement);
void this.getGPUsFromServer().then((gpus: TGpuName[]): void => {
let moreThanOneGPU = false;
@@ -173,101 +371,79 @@ class CrystoolsMonitor {
moreThanOneGPU = true;
}
- gpus?.forEach(({
- name, index
- }) => {
-
- if(name === undefined || index === undefined) {
- console.warn('getGPUsFromServer: name or index undefined', name, index);
- return;
- }
-
- let label = 'GPU';
- let labelVRAM = 'VRAM';
- let labelTemperature = 'Temp';
- if (moreThanOneGPU) {
- label = 'GPU '+index;
- labelVRAM = 'VRAM'+index;
- labelTemperature = 'Temp '+index;
- }
-
- // GPU Utilization Variables
- const monitorGPUNElement: TMonitorSettings = {
- id: 'Crystools.switchGPU' + index,
- name: this.menuPrefix + `[menu] Display GPU\r\n[${index}] ${name}`,
- type: 'boolean',
- label,
- symbol: '%',
- title: `${index}: ${name}`,
- defaultValue: true,
- htmlMonitorRef: undefined,
- htmlMonitorSliderRef: undefined,
- htmlMonitorLabelRef: undefined,
- cssColor: '#0C86F4',
- onChange: async(value: boolean) => {
- this.updateWidget(monitorGPUNElement);
- void await this.updateServerGPU(index,{
- utilization: value
- });
- },
- };
-
- // GPU VRAM Variables
- const monitorVRAMNElement: TMonitorSettings = {
- id: 'Crystools.switchVRAM' + index,
- name: this.menuPrefix + `[menu] Display GPU VRAM\r\n[${index}] ${name}`,
- type: 'boolean',
- label: labelVRAM,
- symbol: '%',
- title: `${index}: ${name}`,
- defaultValue: true,
- htmlMonitorRef: undefined,
- htmlMonitorSliderRef: undefined,
- htmlMonitorLabelRef: undefined,
- cssColor: '#176EC7',
- onChange: async(value: boolean) => {
- this.updateWidget(monitorVRAMNElement);
- void await this.updateServerGPU(index,{
- vram: value
- });
- },
- };
-
- // GPU Temperature Variables
- const monitorTemperatureNElement: TMonitorSettings = {
- id: 'Crystools.switchTemperature' + index,
- name: this.menuPrefix + `[menu] Display GPU Temperature\r\n[${index}] ${name}`,
- type: 'boolean',
- label: labelTemperature,
- symbol: '°',
- title: `${index}: ${name}`,
- defaultValue: true,
- htmlMonitorRef: undefined,
- htmlMonitorSliderRef: undefined,
- htmlMonitorLabelRef: undefined,
- cssColor: '#00ff00',
- cssColorFinal: '#ff0000',
- onChange: async(value: boolean) => {
- this.updateWidget(monitorTemperatureNElement);
- void await this.updateServerGPU(index,{
- temperature: value
- });
- },
- };
-
- this.monitorGPUSettings[index] = monitorGPUNElement;
- this.monitorVRAMSettings[index] = monitorVRAMNElement;
- this.monitorTemperatureSettings[index] = monitorTemperatureNElement;
- // @ts-ignore
- app.ui.settings.addSetting(this.monitorGPUSettings[index]);
- // @ts-ignore
- app.ui.settings.addSetting(this.monitorVRAMSettings[index]);
- // @ts-ignore
- app.ui.settings.addSetting(this.monitorTemperatureSettings[index]);
+ gpus?.forEach(({name, index}) => {
+ this.createSettingsGPUTemp(name, index, moreThanOneGPU);
+ this.createSettingsGPUVRAM(name, index, moreThanOneGPU);
+ this.createSettingsGPUUsage(name, index, moreThanOneGPU);
});
+
+ this.finishedLoad();
});
};
+ finishedLoad = (): void => {
+ this.monitorUI.orderMonitors();
+ this.updateAllWidget();
+ this.moveMonitor(this.menuDisplayOption);
+
+ const w = app.ui.settings.getSettingValue(this.monitorWidthId, this.monitorWidth);
+ const h = app.ui.settings.getSettingValue(this.monitorHeightId, this.monitorHeight);
+ this.monitorUI.updateMonitorSize(w, h);
+ };
+
+ updateDisplay = (value: MenuDisplayOptions): void => {
+ if (value !== this.menuDisplayOption) {
+ this.menuDisplayOption = value;
+ this.moveMonitor(this.menuDisplayOption);
+ }
+ };
+
+ moveMonitor = (position: MenuDisplayOptions): void => {
+ let parentElement: Element | null | undefined;
+
+ switch (position) {
+ case MenuDisplayOptions.Disabled:
+ parentElement = document.getElementById('queue-button');
+ if (parentElement && this.monitorUI.rootElement) {
+ parentElement.insertAdjacentElement('afterend', this.crystoolsButtonGroup.element);
+ } else {
+ console.error('Crystools: parentElement to move monitors not found!', parentElement);
+ }
+ break;
+
+ case MenuDisplayOptions.Top:
+ case MenuDisplayOptions.Bottom:
+ app.menu?.settingsGroup.element.before(this.crystoolsButtonGroup.element);
+ }
+
+ };
+
+ updateAllWidget = (): void => {
+ this.updateWidget(this.monitorCPUElement);
+ this.updateWidget(this.monitorRAMElement);
+ this.updateWidget(this.monitorHDDElement);
+
+ this.monitorGPUSettings.forEach((monitorSettings) => {
+ monitorSettings && this.updateWidget(monitorSettings);
+ });
+ this.monitorVRAMSettings.forEach((monitorSettings) => {
+ monitorSettings && this.updateWidget(monitorSettings);
+ });
+ this.monitorTemperatureSettings.forEach((monitorSettings) => {
+ monitorSettings && this.updateWidget(monitorSettings);
+ });
+ };
+
+ /**
+ * for the settings menu
+ * @param monitorSettings
+ */
+ updateWidget = (monitorSettings: TMonitorSettings): void => {
+ if (this.monitorUI) {
+ const value = app.ui.settings.getSettingValue(monitorSettings.id, monitorSettings.defaultValue);
+ this.monitorUI.showMonitor(monitorSettings, value);
+ }
+ };
updateServer = async(data: TStatsSettings): Promise => {
const resp = await api.fetchApi('/crystools/monitor', {
@@ -301,7 +477,7 @@ class CrystoolsMonitor {
return this.getDataFromServer('GPU');
};
- getDataFromServer = async(what: string): Promise => {
+ getDataFromServer = async (what: string): Promise => {
const resp = await api.fetchApi(`/crystools/monitor/${what}`, {
method: 'GET',
cache: 'no-store',
@@ -312,227 +488,44 @@ class CrystoolsMonitor {
throw new Error(resp.statusText);
};
- updateAllWidget = (): void => {
- this.updateWidget(this.monitorCPUElement);
- this.updateWidget(this.monitorRAMElement);
- this.updateWidget(this.monitorHDDElement);
-
- this.monitorGPUSettings.forEach((monitorSettings) => {
- monitorSettings && this.updateWidget(monitorSettings);
- });
- this.monitorVRAMSettings.forEach((monitorSettings) => {
- monitorSettings && this.updateWidget(monitorSettings);
- });
- this.monitorTemperatureSettings.forEach((monitorSettings) => {
- monitorSettings && this.updateWidget(monitorSettings);
- });
- };
-
- updateWidget = (monitorSettings: TMonitorSettings): void => {
- const value = app.ui.settings.getSettingValue(monitorSettings.id, monitorSettings.defaultValue);
- if (monitorSettings.htmlMonitorRef) {
- monitorSettings.htmlMonitorRef.style.display = value ? 'flex' : 'none';
- }
- };
-
-
- updateAllMonitors = (data: TStatsData): void => {
- // console.log('updateAllMonitors', data);
- this.updateMonitor(this.monitorCPUElement, data.cpu_utilization);
- this.updateMonitor(this.monitorRAMElement, data.ram_used_percent);
- this.updateMonitor(this.monitorHDDElement, data.hdd_used_percent);
-
- if (data.gpus === undefined || data.gpus.length === 0) {
- console.warn('UpdateAllMonitors: no GPU data');
+ setup = (): void => {
+ if (this.monitorUI) {
return;
}
- this.monitorGPUSettings.forEach((monitorSettings, index) => {
- if (data.gpus[index]) {
- const gpu = data.gpus[index];
- if (gpu === undefined) {
- // console.error('UpdateAllMonitors: no GPU data for index', index);
- return;
- }
+ this.createSettingsRate();
+ this.createSettingsMonitorHeight();
+ this.createSettingsMonitorWidth();
+ this.createSettingsCPU();
+ this.createSettingsRAM();
+ this.createSettingsHDD();
+ this.createSettings();
- this.updateMonitor(monitorSettings, gpu.gpu_utilization);
- } else {
- // console.error('UpdateAllMonitors: no GPU data for index', index);
- }
- });
+ const currentRate =
+ parseFloat(app.ui.settings.getSettingValue(this.settingsRate.id, this.settingsRate.defaultValue));
- this.monitorVRAMSettings.forEach((monitorSettings, index) => {
- if (data.gpus[index]) {
- const gpu = data.gpus[index];
- if (gpu === undefined) {
- // console.error('UpdateAllMonitors: no GPU VRAM data for index', index);
- return;
- }
+ this.menuDisplayOption = app.ui.settings.getSettingValue(ComfyKeyMenuDisplayOption, MenuDisplayOptions.Disabled);
+ app.ui.settings.addEventListener(`${ComfyKeyMenuDisplayOption}.change`, (e: any) => {
+ this.updateDisplay(e.detail.value);
+ },
+ );
- this.updateMonitor(monitorSettings, gpu.vram_used_percent);
- } else {
- // console.error('UpdateAllMonitors: no GPU VRAM data for index', index);
- }
- });
+ this.crystoolsButtonGroup = new ComfyButtonGroup();
+ app.menu?.settingsGroup.element.before(this.crystoolsButtonGroup.element);
- this.monitorTemperatureSettings.forEach((monitorSettings, index) => {
- if (data.gpus[index]) {
- const gpu = data.gpus[index];
- if (gpu === undefined) {
- // console.error('UpdateAllMonitors: no GPU VRAM data for index', index);
- return;
- }
+ this.monitorUI = new MonitorUI(
+ this.crystoolsButtonGroup.element,
+ this.monitorCPUElement,
+ this.monitorRAMElement,
+ this.monitorHDDElement,
+ this.monitorGPUSettings,
+ this.monitorVRAMSettings,
+ this.monitorTemperatureSettings,
+ currentRate,
+ );
- this.updateMonitor(monitorSettings, gpu.gpu_temperature);
- if(monitorSettings.cssColorFinal && monitorSettings.htmlMonitorSliderRef){
- monitorSettings.htmlMonitorSliderRef.style.backgroundColor =
- `color-mix(in srgb, ${monitorSettings.cssColorFinal} ${gpu.gpu_temperature}%, ${monitorSettings.cssColor})`;
- }
- } else {
- // console.error('UpdateAllMonitors: no GPU VRAM data for index', index);
- }
- });
- };
-
- updateMonitor = (monitorSettings: TMonitorSettings, percent: number): void => {
- if (!(monitorSettings.htmlMonitorSliderRef && monitorSettings.htmlMonitorLabelRef)) {
- return;
- }
-
- monitorSettings.htmlMonitorLabelRef.innerHTML = `${Math.floor(percent)}${monitorSettings.symbol}`;
- monitorSettings.htmlMonitorSliderRef.style.width = `${Math.floor(percent)}%`;
- };
-
- updateAllAnimationDuration = (value: number): void => {
- this.updatedAnimationDuration(this.monitorCPUElement, value);
- this.updatedAnimationDuration(this.monitorRAMElement, value);
- this.updatedAnimationDuration(this.monitorHDDElement, value);
- this.monitorGPUSettings.forEach((monitorSettings) => {
- monitorSettings && this.updatedAnimationDuration(monitorSettings, value);
- });
- this.monitorVRAMSettings.forEach((monitorSettings) => {
- monitorSettings && this.updatedAnimationDuration(monitorSettings, value);
- });
- this.monitorTemperatureSettings.forEach((monitorSettings) => {
- monitorSettings && this.updatedAnimationDuration(monitorSettings, value);
- });
- };
-
- updatedAnimationDuration = (monitorSettings: TMonitorSettings, value: number): void => {
- const slider = monitorSettings.htmlMonitorSliderRef;
- if (!slider) {
- return;
- }
-
- slider.style.transition = `width ${value.toFixed(1)}s`;
- };
-
- setup(): void {
- const parentElement = document.getElementById('queue-button');
-
- let ctoolsRoot = document.getElementById(this.htmlIdCrystoolsRoot);
- if (!ctoolsRoot) {
- ctoolsRoot = document.createElement('div');
- ctoolsRoot.setAttribute('id', this.htmlIdCrystoolsRoot);
- ctoolsRoot.style.display = 'flex';
- ctoolsRoot.style.width = '100%';
- ctoolsRoot.style.flexDirection = 'column';
- parentElement?.insertAdjacentElement('afterend', ctoolsRoot);
- }
-
- const htmlContainer = document.createElement('div');
- htmlContainer.setAttribute('id', this.htmlIdCrystoolsMonitorContainer);
- htmlContainer.style.width = '100%';
- htmlContainer.style.cursor = 'crosshair';
- htmlContainer.style.order = '3';
- htmlContainer.style.margin = '4px 0';
- ctoolsRoot.append(htmlContainer);
-
- htmlContainer.append(this.createMonitor(this.monitorCPUElement));
- htmlContainer.append(this.createMonitor(this.monitorRAMElement));
-
- // gpu0 > gpu1 > vram0 > vram1
- // this.monitorGPUSettings.forEach((monitorSettings) => {
- // monitorSettings && htmlContainer.append(this.createMonitor(monitorSettings));
- // });
- // this.monitorVRAMSettings.forEach((monitorSettings) => {
- // monitorSettings && htmlContainer.append(this.createMonitor(monitorSettings));
- // });
- // this.monitorTemperatureSettings.forEach((monitorSettings) => {
- // monitorSettings && htmlContainer.append(this.createMonitor(monitorSettings));
- // });
-
- // gpu0 > vram0 > gpu1 > vram1
- this.monitorGPUSettings.forEach((_monitorSettings, index) => {
- this.monitorGPUSettings[index] && htmlContainer.append(this.createMonitor(this.monitorGPUSettings[index]));
- this.monitorVRAMSettings[index] && htmlContainer.append(this.createMonitor(this.monitorVRAMSettings[index]));
- this.monitorTemperatureSettings[index] &&
- htmlContainer.append(this.createMonitor(this.monitorTemperatureSettings[index]));
- });
-
- htmlContainer.append(this.createMonitor(this.monitorHDDElement));
-
- const currentRate = parseFloat(app.ui.settings.getSettingValue(this.idInputRate, this.defaultRate));
- this.updateAllAnimationDuration(currentRate);
- this.updateAllWidget();
+ this.updateDisplay(this.menuDisplayOption);
this.registerListeners();
- }
-
- createMonitor = (monitorSettings?: TMonitorSettings): HTMLDivElement => {
- if (!monitorSettings) {
- // just for typescript
- return document.createElement('div');
- }
- const htmlMain = document.createElement('div');
- htmlMain.setAttribute('id', monitorSettings.id);
- htmlMain.style.margin = '2px 10px';
- htmlMain.style.height = '12px';
- htmlMain.style.position = 'relative';
- htmlMain.style.display = 'flex';
- htmlMain.style.alignItems = 'center';
- htmlMain.style.flexDirection = 'row';
- monitorSettings.htmlMonitorRef = htmlMain;
-
- if (monitorSettings.title) {
- htmlMain.title = monitorSettings.title;
- }
-
- const htmlMonitorText = document.createElement('div');
- htmlMonitorText.style.width = '35px';
- htmlMonitorText.style.fontSize = '10px';
- htmlMonitorText.innerHTML = monitorSettings.label;
- htmlMain.append(htmlMonitorText);
-
- const htmlMonitorContent = document.createElement('div');
- htmlMonitorContent.style.height = '100%';
- htmlMonitorContent.style.flexGrow = '1';
- htmlMonitorContent.style.position = 'relative';
- htmlMonitorContent.style.backgroundColor = 'var(--bg-color)';
- htmlMain.append(htmlMonitorContent);
-
- const htmlMonitorSlider = document.createElement('div');
- htmlMonitorSlider.style.position = 'absolute';
- htmlMonitorSlider.style.height = '100%';
- htmlMonitorSlider.style.width = '0';
- if(monitorSettings.cssColorFinal){
- htmlMonitorSlider.style.backgroundColor =
- `color-mix(in srgb, ${monitorSettings.cssColorFinal} 0%, ${monitorSettings.cssColor})`;
- } else {
- htmlMonitorSlider.style.backgroundColor = monitorSettings.cssColor;
- }
- monitorSettings.htmlMonitorSliderRef = htmlMonitorSlider;
- htmlMonitorContent.append(htmlMonitorSlider);
-
- const htmlMonitorLabel = document.createElement('div');
- htmlMonitorLabel.style.position = 'relative';
- htmlMonitorLabel.style.width = '100%';
- htmlMonitorLabel.style.color = 'var(--drag-text)';
- htmlMonitorLabel.style.fontSize = '10px';
- htmlMonitorLabel.innerHTML = '0%';
- monitorSettings.htmlMonitorLabelRef = htmlMonitorLabel;
- htmlMonitorContent.append(htmlMonitorLabel);
-
- return monitorSettings.htmlMonitorRef;
};
registerListeners = (): void => {
@@ -540,7 +533,7 @@ class CrystoolsMonitor {
if (event?.detail === undefined) {
return;
}
- this.updateAllMonitors(event.detail);
+ this.monitorUI.updateDisplay(event.detail);
}, false);
};
}
@@ -548,5 +541,5 @@ class CrystoolsMonitor {
const crystoolsMonitor = new CrystoolsMonitor();
app.registerExtension({
name: crystoolsMonitor.idExtensionName,
- setup: crystoolsMonitor.setup.bind(crystoolsMonitor),
+ setup: crystoolsMonitor.setup,
});
diff --git a/web/monitorUI.d.ts b/web/monitorUI.d.ts
new file mode 100644
index 0000000..c3486e8
--- /dev/null
+++ b/web/monitorUI.d.ts
@@ -0,0 +1,24 @@
+import { ProgressBarUIBase } from './progressBarUIBase.js';
+export declare class MonitorUI extends ProgressBarUIBase {
+ rootElement: HTMLElement;
+ private monitorCPUElement;
+ private monitorRAMElement;
+ private monitorHDDElement;
+ private monitorGPUSettings;
+ private monitorVRAMSettings;
+ private monitorTemperatureSettings;
+ private currentRate;
+ lastMonitor: number;
+ styleSheet: HTMLStyleElement;
+ constructor(rootElement: HTMLElement, monitorCPUElement: TMonitorSettings, monitorRAMElement: TMonitorSettings, monitorHDDElement: TMonitorSettings, monitorGPUSettings: TMonitorSettings[], monitorVRAMSettings: TMonitorSettings[], monitorTemperatureSettings: TMonitorSettings[], currentRate: number);
+ createDOM: () => void;
+ createDOMGPUMonitor: (monitorSettings?: TMonitorSettings) => void;
+ orderMonitors: () => void;
+ updateDisplay: (data: TStatsData) => void;
+ updateMonitor: (monitorSettings: TMonitorSettings, percent: number, used?: number, total?: number) => void;
+ updateAllAnimationDuration: (value: number) => void;
+ updatedAnimationDuration: (monitorSettings: TMonitorSettings, value: number) => void;
+ createMonitor: (monitorSettings?: TMonitorSettings) => HTMLDivElement;
+ updateMonitorSize: (width: number, height: number) => void;
+ showMonitor: (monitorSettings: TMonitorSettings, value: boolean) => void;
+}
diff --git a/web/monitorUI.js b/web/monitorUI.js
new file mode 100644
index 0000000..ec37f5f
--- /dev/null
+++ b/web/monitorUI.js
@@ -0,0 +1,281 @@
+import { ProgressBarUIBase } from './progressBarUIBase.js';
+import { createStyleSheet, formatBytes } from './utils.js';
+export class MonitorUI extends ProgressBarUIBase {
+ constructor(rootElement, monitorCPUElement, monitorRAMElement, monitorHDDElement, monitorGPUSettings, monitorVRAMSettings, monitorTemperatureSettings, currentRate) {
+ super('crystools-monitors-root', rootElement);
+ Object.defineProperty(this, "rootElement", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: rootElement
+ });
+ Object.defineProperty(this, "monitorCPUElement", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: monitorCPUElement
+ });
+ Object.defineProperty(this, "monitorRAMElement", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: monitorRAMElement
+ });
+ Object.defineProperty(this, "monitorHDDElement", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: monitorHDDElement
+ });
+ Object.defineProperty(this, "monitorGPUSettings", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: monitorGPUSettings
+ });
+ Object.defineProperty(this, "monitorVRAMSettings", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: monitorVRAMSettings
+ });
+ Object.defineProperty(this, "monitorTemperatureSettings", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: monitorTemperatureSettings
+ });
+ Object.defineProperty(this, "currentRate", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: currentRate
+ });
+ Object.defineProperty(this, "lastMonitor", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: 1
+ });
+ Object.defineProperty(this, "styleSheet", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: void 0
+ });
+ Object.defineProperty(this, "createDOM", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: () => {
+ if (!this.rootElement) {
+ throw Error('Crystools: MonitorUI - Container not found');
+ }
+ this.rootElement.appendChild(this.createMonitor(this.monitorCPUElement));
+ this.rootElement.appendChild(this.createMonitor(this.monitorRAMElement));
+ this.rootElement.appendChild(this.createMonitor(this.monitorHDDElement));
+ this.updateAllAnimationDuration(this.currentRate);
+ }
+ });
+ Object.defineProperty(this, "createDOMGPUMonitor", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: (monitorSettings) => {
+ if (!(monitorSettings && this.rootElement)) {
+ return;
+ }
+ this.rootElement.appendChild(this.createMonitor(monitorSettings));
+ this.updateAllAnimationDuration(this.currentRate);
+ }
+ });
+ Object.defineProperty(this, "orderMonitors", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: () => {
+ try {
+ this.monitorCPUElement.htmlMonitorRef.style.order = '' + this.lastMonitor++;
+ this.monitorRAMElement.htmlMonitorRef.style.order = '' + this.lastMonitor++;
+ this.monitorGPUSettings.forEach((_monitorSettings, index) => {
+ this.monitorGPUSettings[index].htmlMonitorRef.style.order = '' + this.lastMonitor++;
+ this.monitorVRAMSettings[index].htmlMonitorRef.style.order = '' + this.lastMonitor++;
+ this.monitorTemperatureSettings[index].htmlMonitorRef.style.order = '' + this.lastMonitor++;
+ });
+ this.monitorHDDElement.htmlMonitorRef.style.order = '' + this.lastMonitor++;
+ }
+ catch (error) {
+ console.error('orderMonitors', error);
+ }
+ }
+ });
+ Object.defineProperty(this, "updateDisplay", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: (data) => {
+ this.updateMonitor(this.monitorCPUElement, data.cpu_utilization);
+ this.updateMonitor(this.monitorRAMElement, data.ram_used_percent, data.ram_used, data.ram_total);
+ this.updateMonitor(this.monitorHDDElement, data.hdd_used_percent, data.hdd_used, data.hdd_total);
+ if (data.gpus === undefined || data.gpus.length === 0) {
+ console.warn('UpdateAllMonitors: no GPU data');
+ return;
+ }
+ this.monitorGPUSettings.forEach((monitorSettings, index) => {
+ if (data.gpus[index]) {
+ const gpu = data.gpus[index];
+ if (gpu === undefined) {
+ return;
+ }
+ this.updateMonitor(monitorSettings, gpu.gpu_utilization);
+ }
+ else {
+ }
+ });
+ this.monitorVRAMSettings.forEach((monitorSettings, index) => {
+ if (data.gpus[index]) {
+ const gpu = data.gpus[index];
+ if (gpu === undefined) {
+ return;
+ }
+ this.updateMonitor(monitorSettings, gpu.vram_used_percent, gpu.vram_used, gpu.vram_total);
+ }
+ else {
+ }
+ });
+ this.monitorTemperatureSettings.forEach((monitorSettings, index) => {
+ if (data.gpus[index]) {
+ const gpu = data.gpus[index];
+ if (gpu === undefined) {
+ return;
+ }
+ this.updateMonitor(monitorSettings, gpu.gpu_temperature);
+ if (monitorSettings.cssColorFinal && monitorSettings.htmlMonitorSliderRef) {
+ monitorSettings.htmlMonitorSliderRef.style.backgroundColor =
+ `color-mix(in srgb, ${monitorSettings.cssColorFinal} ${gpu.gpu_temperature}%, ${monitorSettings.cssColor})`;
+ }
+ }
+ else {
+ }
+ });
+ }
+ });
+ Object.defineProperty(this, "updateMonitor", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: (monitorSettings, percent, used, total) => {
+ if (!(monitorSettings.htmlMonitorSliderRef && monitorSettings.htmlMonitorLabelRef)) {
+ return;
+ }
+ if (percent < 0) {
+ return;
+ }
+ const prefix = monitorSettings.monitorTitle ? monitorSettings.monitorTitle + ' - ' : '';
+ let title = `${Math.floor(percent)}${monitorSettings.symbol}`;
+ let postfix = '';
+ if (used !== undefined && total !== undefined) {
+ postfix = ` - ${formatBytes(used)} / ${formatBytes(total)} GB`;
+ }
+ title = `${prefix}${title}${postfix}`;
+ if (monitorSettings.htmlMonitorRef) {
+ monitorSettings.htmlMonitorRef.title = title;
+ }
+ monitorSettings.htmlMonitorLabelRef.innerHTML = `${Math.floor(percent)}${monitorSettings.symbol}`;
+ monitorSettings.htmlMonitorSliderRef.style.width = `${Math.floor(percent)}%`;
+ }
+ });
+ Object.defineProperty(this, "updateAllAnimationDuration", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: (value) => {
+ this.updatedAnimationDuration(this.monitorCPUElement, value);
+ this.updatedAnimationDuration(this.monitorRAMElement, value);
+ this.updatedAnimationDuration(this.monitorHDDElement, value);
+ this.monitorGPUSettings.forEach((monitorSettings) => {
+ monitorSettings && this.updatedAnimationDuration(monitorSettings, value);
+ });
+ this.monitorVRAMSettings.forEach((monitorSettings) => {
+ monitorSettings && this.updatedAnimationDuration(monitorSettings, value);
+ });
+ this.monitorTemperatureSettings.forEach((monitorSettings) => {
+ monitorSettings && this.updatedAnimationDuration(monitorSettings, value);
+ });
+ }
+ });
+ Object.defineProperty(this, "updatedAnimationDuration", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: (monitorSettings, value) => {
+ const slider = monitorSettings.htmlMonitorSliderRef;
+ if (!slider) {
+ return;
+ }
+ slider.style.transition = `width ${value.toFixed(1)}s`;
+ }
+ });
+ Object.defineProperty(this, "createMonitor", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: (monitorSettings) => {
+ if (!monitorSettings) {
+ return document.createElement('div');
+ }
+ const htmlMain = document.createElement('div');
+ htmlMain.classList.add(monitorSettings.id);
+ htmlMain.classList.add('crystools-monitor');
+ monitorSettings.htmlMonitorRef = htmlMain;
+ if (monitorSettings.title) {
+ htmlMain.title = monitorSettings.title;
+ }
+ const htmlMonitorText = document.createElement('div');
+ htmlMonitorText.classList.add('crystools-text');
+ htmlMonitorText.innerHTML = monitorSettings.label;
+ htmlMain.append(htmlMonitorText);
+ const htmlMonitorContent = document.createElement('div');
+ htmlMonitorContent.classList.add('crystools-content');
+ htmlMain.append(htmlMonitorContent);
+ const htmlMonitorSlider = document.createElement('div');
+ htmlMonitorSlider.classList.add('crystools-slider');
+ if (monitorSettings.cssColorFinal) {
+ htmlMonitorSlider.style.backgroundColor =
+ `color-mix(in srgb, ${monitorSettings.cssColorFinal} 0%, ${monitorSettings.cssColor})`;
+ }
+ else {
+ htmlMonitorSlider.style.backgroundColor = monitorSettings.cssColor;
+ }
+ monitorSettings.htmlMonitorSliderRef = htmlMonitorSlider;
+ htmlMonitorContent.append(htmlMonitorSlider);
+ const htmlMonitorLabel = document.createElement('div');
+ htmlMonitorLabel.classList.add('crystools-label');
+ monitorSettings.htmlMonitorLabelRef = htmlMonitorLabel;
+ htmlMonitorContent.append(htmlMonitorLabel);
+ htmlMonitorLabel.innerHTML = '0%';
+ return monitorSettings.htmlMonitorRef;
+ }
+ });
+ Object.defineProperty(this, "updateMonitorSize", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: (width, height) => {
+ this.styleSheet.innerText = `.comfyui-menu #crystools-monitors-root .crystools-monitor .crystools-content {height: ${height}px; width: ${width}px;}`;
+ }
+ });
+ Object.defineProperty(this, "showMonitor", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: (monitorSettings, value) => {
+ if (monitorSettings.htmlMonitorRef) {
+ monitorSettings.htmlMonitorRef.style.display = value ? 'flex' : 'none';
+ }
+ }
+ });
+ this.createDOM();
+ this.styleSheet = createStyleSheet('crystools-monitors-size');
+ }
+}
diff --git a/web/monitorUI.ts b/web/monitorUI.ts
new file mode 100644
index 0000000..ecef44f
--- /dev/null
+++ b/web/monitorUI.ts
@@ -0,0 +1,229 @@
+import { ProgressBarUIBase } from './progressBarUIBase.js';
+import { createStyleSheet, formatBytes } from './utils.js';
+
+export class MonitorUI extends ProgressBarUIBase {
+ lastMonitor = 1; // just for order on monitors section
+ styleSheet: HTMLStyleElement;
+
+ constructor(
+ public override rootElement: HTMLElement,
+ private monitorCPUElement: TMonitorSettings,
+ private monitorRAMElement: TMonitorSettings,
+ private monitorHDDElement: TMonitorSettings,
+ private monitorGPUSettings: TMonitorSettings[],
+ private monitorVRAMSettings: TMonitorSettings[],
+ private monitorTemperatureSettings: TMonitorSettings[],
+ private currentRate: number,
+ ) {
+ super('crystools-monitors-root', rootElement);
+ this.createDOM();
+
+ this.styleSheet = createStyleSheet('crystools-monitors-size');
+ }
+
+ createDOM = (): void => {
+ if (!this.rootElement) {
+ throw Error('Crystools: MonitorUI - Container not found');
+ }
+
+ // this.container.style.order = '2';
+ this.rootElement.appendChild(this.createMonitor(this.monitorCPUElement));
+ this.rootElement.appendChild(this.createMonitor(this.monitorRAMElement));
+ this.rootElement.appendChild(this.createMonitor(this.monitorHDDElement));
+ this.updateAllAnimationDuration(this.currentRate);
+ };
+
+ createDOMGPUMonitor = (monitorSettings?: TMonitorSettings): void => {
+ if (!(monitorSettings && this.rootElement)) {
+ return;
+ }
+
+ this.rootElement.appendChild(this.createMonitor(monitorSettings));
+ this.updateAllAnimationDuration(this.currentRate);
+ };
+
+ orderMonitors = (): void => {
+ try {
+ // @ts-ignore
+ this.monitorCPUElement.htmlMonitorRef.style.order = '' + this.lastMonitor++;
+ // @ts-ignore
+ this.monitorRAMElement.htmlMonitorRef.style.order = '' + this.lastMonitor++;
+ // @ts-ignore
+ this.monitorGPUSettings.forEach((_monitorSettings, index) => {
+ // @ts-ignore
+ this.monitorGPUSettings[index].htmlMonitorRef.style.order = '' + this.lastMonitor++;
+ // @ts-ignore
+ this.monitorVRAMSettings[index].htmlMonitorRef.style.order = '' + this.lastMonitor++;
+ // @ts-ignore
+ this.monitorTemperatureSettings[index].htmlMonitorRef.style.order = '' + this.lastMonitor++;
+ });
+ // @ts-ignore
+ this.monitorHDDElement.htmlMonitorRef.style.order = '' + this.lastMonitor++;
+ } catch (error) {
+ console.error('orderMonitors', error);
+ }
+ };
+
+ updateDisplay = (data: TStatsData): void => {
+ this.updateMonitor(this.monitorCPUElement, data.cpu_utilization);
+ this.updateMonitor(this.monitorRAMElement, data.ram_used_percent, data.ram_used, data.ram_total);
+ this.updateMonitor(this.monitorHDDElement, data.hdd_used_percent, data.hdd_used, data.hdd_total);
+
+ if (data.gpus === undefined || data.gpus.length === 0) {
+ console.warn('UpdateAllMonitors: no GPU data');
+ return;
+ }
+
+ this.monitorGPUSettings.forEach((monitorSettings, index) => {
+ if (data.gpus[index]) {
+ const gpu = data.gpus[index];
+ if (gpu === undefined) {
+ // console.error('UpdateAllMonitors: no GPU data for index', index);
+ return;
+ }
+
+ this.updateMonitor(monitorSettings, gpu.gpu_utilization);
+ } else {
+ // console.error('UpdateAllMonitors: no GPU data for index', index);
+ }
+ });
+
+ this.monitorVRAMSettings.forEach((monitorSettings, index) => {
+ if (data.gpus[index]) {
+ const gpu = data.gpus[index];
+ if (gpu === undefined) {
+ // console.error('UpdateAllMonitors: no GPU VRAM data for index', index);
+ return;
+ }
+
+ this.updateMonitor(monitorSettings, gpu.vram_used_percent, gpu.vram_used, gpu.vram_total);
+ } else {
+ // console.error('UpdateAllMonitors: no GPU VRAM data for index', index);
+ }
+ });
+
+ this.monitorTemperatureSettings.forEach((monitorSettings, index) => {
+ if (data.gpus[index]) {
+ const gpu = data.gpus[index];
+ if (gpu === undefined) {
+ // console.error('UpdateAllMonitors: no GPU VRAM data for index', index);
+ return;
+ }
+
+ this.updateMonitor(monitorSettings, gpu.gpu_temperature);
+ if (monitorSettings.cssColorFinal && monitorSettings.htmlMonitorSliderRef) {
+ monitorSettings.htmlMonitorSliderRef.style.backgroundColor =
+ `color-mix(in srgb, ${monitorSettings.cssColorFinal} ${gpu.gpu_temperature}%, ${monitorSettings.cssColor})`;
+ }
+ } else {
+ // console.error('UpdateAllMonitors: no GPU VRAM data for index', index);
+ }
+ });
+ };
+
+ updateMonitor = (monitorSettings: TMonitorSettings, percent: number, used?: number, total?: number): void => {
+ if (!(monitorSettings.htmlMonitorSliderRef && monitorSettings.htmlMonitorLabelRef)) {
+ return;
+ }
+
+ if (percent < 0) {
+ return;
+ }
+
+ // console.log('updateMonitor', monitorSettings.id, percent);
+
+ const prefix = monitorSettings.monitorTitle ? monitorSettings.monitorTitle + ' - ' : '';
+ let title = `${Math.floor(percent)}${monitorSettings.symbol}`;
+ let postfix = '';
+
+ if (used !== undefined && total !== undefined) {
+ postfix = ` - ${formatBytes(used)} / ${formatBytes(total)} GB`;
+ }
+ title = `${prefix}${title}${postfix}`;
+
+ if (monitorSettings.htmlMonitorRef) {
+ monitorSettings.htmlMonitorRef.title = title;
+ }
+ monitorSettings.htmlMonitorLabelRef.innerHTML = `${Math.floor(percent)}${monitorSettings.symbol}`;
+ monitorSettings.htmlMonitorSliderRef.style.width = `${Math.floor(percent)}%`;
+ };
+
+ updateAllAnimationDuration = (value: number): void => {
+ this.updatedAnimationDuration(this.monitorCPUElement, value);
+ this.updatedAnimationDuration(this.monitorRAMElement, value);
+ this.updatedAnimationDuration(this.monitorHDDElement, value);
+ this.monitorGPUSettings.forEach((monitorSettings) => {
+ monitorSettings && this.updatedAnimationDuration(monitorSettings, value);
+ });
+ this.monitorVRAMSettings.forEach((monitorSettings) => {
+ monitorSettings && this.updatedAnimationDuration(monitorSettings, value);
+ });
+ this.monitorTemperatureSettings.forEach((monitorSettings) => {
+ monitorSettings && this.updatedAnimationDuration(monitorSettings, value);
+ });
+ };
+
+ updatedAnimationDuration = (monitorSettings: TMonitorSettings, value: number): void => {
+ const slider = monitorSettings.htmlMonitorSliderRef;
+ if (!slider) {
+ return;
+ }
+
+ slider.style.transition = `width ${value.toFixed(1)}s`;
+ };
+
+ createMonitor = (monitorSettings?: TMonitorSettings): HTMLDivElement => {
+ if (!monitorSettings) {
+ // just for typescript
+ return document.createElement('div');
+ }
+
+ const htmlMain = document.createElement('div');
+ htmlMain.classList.add(monitorSettings.id);
+ htmlMain.classList.add('crystools-monitor');
+
+ monitorSettings.htmlMonitorRef = htmlMain;
+
+ if (monitorSettings.title) {
+ htmlMain.title = monitorSettings.title;
+ }
+
+ const htmlMonitorText = document.createElement('div');
+ htmlMonitorText.classList.add('crystools-text');
+ htmlMonitorText.innerHTML = monitorSettings.label;
+ htmlMain.append(htmlMonitorText);
+
+ const htmlMonitorContent = document.createElement('div');
+ htmlMonitorContent.classList.add('crystools-content');
+ htmlMain.append(htmlMonitorContent);
+
+ const htmlMonitorSlider = document.createElement('div');
+ htmlMonitorSlider.classList.add('crystools-slider');
+ if (monitorSettings.cssColorFinal) {
+ htmlMonitorSlider.style.backgroundColor =
+ `color-mix(in srgb, ${monitorSettings.cssColorFinal} 0%, ${monitorSettings.cssColor})`;
+ } else {
+ htmlMonitorSlider.style.backgroundColor = monitorSettings.cssColor;
+ }
+ monitorSettings.htmlMonitorSliderRef = htmlMonitorSlider;
+ htmlMonitorContent.append(htmlMonitorSlider);
+
+ const htmlMonitorLabel = document.createElement('div');
+ htmlMonitorLabel.classList.add('crystools-label');
+ monitorSettings.htmlMonitorLabelRef = htmlMonitorLabel;
+ htmlMonitorContent.append(htmlMonitorLabel);
+ htmlMonitorLabel.innerHTML = '0%';
+ return monitorSettings.htmlMonitorRef;
+ };
+
+ updateMonitorSize = (width: number, height: number): void => {
+ // eslint-disable-next-line max-len
+ this.styleSheet.innerText = `.comfyui-menu #crystools-monitors-root .crystools-monitor .crystools-content {height: ${height}px; width: ${width}px;}`;
+ };
+
+ showMonitor = (monitorSettings: TMonitorSettings, value: boolean): void => {
+ if (monitorSettings.htmlMonitorRef) {
+ monitorSettings.htmlMonitorRef.style.display = value ? 'flex' : 'none';
+ }
+ };
+}
diff --git a/web/progressBar.js b/web/progressBar.js
index 7cdcd9a..6c3e214 100644
--- a/web/progressBar.js
+++ b/web/progressBar.js
@@ -1,12 +1,7 @@
-import { api } from '/scripts/api.js';
-import { app } from '/scripts/app.js';
+import { app, api } from './comfy/index.js';
import { commonPrefix } from './common.js';
-var EStatus;
-(function (EStatus) {
- EStatus["executing"] = "Executing";
- EStatus["executed"] = "Executed";
- EStatus["execution_error"] = "Execution error";
-})(EStatus || (EStatus = {}));
+import { ProgressBarUI } from './progressBarUI.js';
+import { ComfyKeyMenuDisplayOption, EStatus, MenuDisplayOptions } from './progressBarUIBase.js';
class CrystoolsProgressBar {
constructor() {
Object.defineProperty(this, "idExtensionName", {
@@ -19,7 +14,7 @@ class CrystoolsProgressBar {
enumerable: true,
configurable: true,
writable: true,
- value: 'Crystools.showStatus'
+ value: 'Crystools.ProgressBar'
});
Object.defineProperty(this, "defaultShowStatus", {
enumerable: true,
@@ -33,17 +28,11 @@ class CrystoolsProgressBar {
writable: true,
value: commonPrefix
});
- Object.defineProperty(this, "htmlIdCrystoolsRoot", {
+ Object.defineProperty(this, "menuDisplayOption", {
enumerable: true,
configurable: true,
writable: true,
- value: 'crystools-root'
- });
- Object.defineProperty(this, "htmlIdCrystoolsProgressBarContainer", {
- enumerable: true,
- configurable: true,
- writable: true,
- value: 'crystools-progress-bar-container'
+ value: MenuDisplayOptions.Disabled
});
Object.defineProperty(this, "currentStatus", {
enumerable: true,
@@ -69,17 +58,11 @@ class CrystoolsProgressBar {
writable: true,
value: 0
});
- Object.defineProperty(this, "htmlProgressSliderRef", {
+ Object.defineProperty(this, "progressBarUI", {
enumerable: true,
configurable: true,
writable: true,
- value: undefined
- });
- Object.defineProperty(this, "htmlProgressLabelRef", {
- enumerable: true,
- configurable: true,
- writable: true,
- value: undefined
+ value: void 0
});
Object.defineProperty(this, "createSettings", {
enumerable: true,
@@ -88,50 +71,58 @@ class CrystoolsProgressBar {
value: () => {
app.ui.settings.addSetting({
id: this.idShowProgressBar,
- name: this.menuPrefix + '[monitor] Show progress bar',
+ name: 'Show progress bar',
+ category: ['Crystools', this.menuPrefix + ' Progress Bar', 'Show'],
+ tooltip: 'This apply only on "Disabled" (old) menu',
type: 'boolean',
defaultValue: this.defaultShowStatus,
- onChange: this.showProgressBar,
+ onChange: this.progressBarUI.showProgressBar,
});
}
});
- Object.defineProperty(this, "showProgressBar", {
- enumerable: true,
- configurable: true,
- writable: true,
- value: (value) => {
- const container = document.getElementById(this.htmlIdCrystoolsProgressBarContainer);
- if (container) {
- container.style.display = value ? 'block' : 'none';
- }
- }
- });
Object.defineProperty(this, "updateDisplay", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: (menuDisplayOption) => {
+ if (menuDisplayOption !== this.menuDisplayOption) {
+ this.menuDisplayOption = menuDisplayOption;
+ this.progressBarUI.showSection(this.menuDisplayOption === MenuDisplayOptions.Disabled);
+ }
+ if (this.menuDisplayOption === MenuDisplayOptions.Disabled && this.progressBarUI.showProgressBarFlag) {
+ console.log('updateDisplay progress bar');
+ this.progressBarUI.updateDisplay(this.currentStatus, this.timeStart, this.currentProgress);
+ }
+ }
+ });
+ Object.defineProperty(this, "setup", {
enumerable: true,
configurable: true,
writable: true,
value: () => {
- if (!(this.htmlProgressLabelRef && this.htmlProgressSliderRef)) {
- console.error('htmlProgressLabelRef or htmlProgressSliderRef is undefined');
+ if (this.progressBarUI) {
+ this.progressBarUI
+ .showProgressBar(app.ui.settings.getSettingValue(this.idShowProgressBar, this.defaultShowStatus));
return;
}
- if (this.currentStatus === EStatus.executed) {
- this.htmlProgressLabelRef.innerHTML = 'cached';
- const timeElapsed = Date.now() - this.timeStart;
- if (this.timeStart > 0 && timeElapsed > 0) {
- this.htmlProgressLabelRef.innerHTML = new Date(timeElapsed).toISOString().substr(11, 8);
- }
- this.htmlProgressSliderRef.style.width = '0';
+ this.menuDisplayOption = app.ui.settings.getSettingValue(ComfyKeyMenuDisplayOption, MenuDisplayOptions.Disabled);
+ app.ui.settings.addEventListener(`${ComfyKeyMenuDisplayOption}.change`, (e) => {
+ console.log('Comfy.UseNewMenu.change desde progress bar', e.detail.value);
+ this.updateDisplay(e.detail.value);
+ });
+ const progressBarElement = document.createElement('div');
+ progressBarElement.classList.add('crystools-monitors-container');
+ this.progressBarUI = new ProgressBarUI(progressBarElement, (this.menuDisplayOption === MenuDisplayOptions.Disabled), this.centerNode);
+ const parentElement = document.getElementById('queue-button');
+ if (parentElement) {
+ parentElement.insertAdjacentElement('afterend', progressBarElement);
}
- else if (this.currentStatus === EStatus.execution_error) {
- this.htmlProgressLabelRef.innerHTML = 'ERROR';
- this.htmlProgressSliderRef.style.backgroundColor = 'var(--error-text)';
- }
- else if (this.currentStatus === EStatus.executing) {
- this.htmlProgressLabelRef.innerHTML = `${this.currentProgress}%`;
- this.htmlProgressSliderRef.style.width = this.htmlProgressLabelRef.innerHTML;
- this.htmlProgressSliderRef.style.backgroundColor = 'green';
+ else {
+ console.error('Crystools: parentElement to move monitors not found!', parentElement);
}
+ this.createSettings();
+ this.updateDisplay(this.menuDisplayOption);
+ this.registerListeners();
}
});
Object.defineProperty(this, "registerListeners", {
@@ -139,37 +130,37 @@ class CrystoolsProgressBar {
configurable: true,
writable: true,
value: () => {
- api.addEventListener('status', ({ detail, }) => {
+ api.addEventListener('status', ({ detail }) => {
this.currentStatus = this.currentStatus === EStatus.execution_error ? EStatus.execution_error : EStatus.executed;
const queueRemaining = detail?.exec_info.queue_remaining;
if (queueRemaining) {
this.currentStatus = EStatus.executing;
}
- this.updateDisplay();
+ this.updateDisplay(this.menuDisplayOption);
}, false);
- api.addEventListener('progress', ({ detail, }) => {
- const { value, max, node, } = detail;
+ api.addEventListener('progress', ({ detail }) => {
+ const { value, max, node } = detail;
const progress = Math.floor((value / max) * 100);
if (!isNaN(progress) && progress >= 0 && progress <= 100) {
this.currentProgress = progress;
this.currentNode = node;
}
- this.updateDisplay();
+ this.updateDisplay(this.menuDisplayOption);
}, false);
- api.addEventListener('executed', ({ detail, }) => {
+ api.addEventListener('executed', ({ detail }) => {
if (detail?.node) {
this.currentNode = detail.node;
}
- this.updateDisplay();
+ this.updateDisplay(this.menuDisplayOption);
}, false);
- api.addEventListener('execution_start', ({ _detail, }) => {
+ api.addEventListener('execution_start', ({ _detail }) => {
this.currentStatus = EStatus.executing;
this.timeStart = Date.now();
- this.updateDisplay();
+ this.updateDisplay(this.menuDisplayOption);
}, false);
- api.addEventListener('execution_error', ({ _detail, }) => {
+ api.addEventListener('execution_error', ({ _detail }) => {
this.currentStatus = EStatus.execution_error;
- this.updateDisplay();
+ this.updateDisplay(this.menuDisplayOption);
}, false);
}
});
@@ -189,61 +180,10 @@ class CrystoolsProgressBar {
app.canvas.centerOnNode(node);
}
});
- this.createSettings();
- }
- setup() {
- const parentElement = document.getElementById('queue-button');
- if (!parentElement) {
- console.error('queue-button not found');
- return;
- }
- let ctoolsRoot = document.getElementById(this.htmlIdCrystoolsRoot);
- if (!ctoolsRoot) {
- ctoolsRoot = document.createElement('div');
- ctoolsRoot.setAttribute('id', this.htmlIdCrystoolsRoot);
- ctoolsRoot.style.display = 'flex';
- ctoolsRoot.style.width = '100%';
- ctoolsRoot.style.flexDirection = 'column';
- parentElement.insertAdjacentElement('afterend', ctoolsRoot);
- }
- const htmlContainer = document.createElement('div');
- htmlContainer.setAttribute('id', this.htmlIdCrystoolsProgressBarContainer);
- htmlContainer.setAttribute('title', 'click to see the current working node');
- htmlContainer.style.margin = '4px 0';
- htmlContainer.style.width = '100%';
- htmlContainer.style.cursor = 'pointer';
- htmlContainer.style.order = '1';
- htmlContainer.addEventListener('click', this.centerNode);
- ctoolsRoot.append(htmlContainer);
- const progressBar = document.createElement('div');
- progressBar.style.margin = '0 10px';
- progressBar.style.height = '18px';
- progressBar.style.position = 'relative';
- progressBar.style.backgroundColor = 'var(--bg-color)';
- htmlContainer.append(progressBar);
- const progressSlider = document.createElement('div');
- progressSlider.style.position = 'absolute';
- progressSlider.style.height = '100%';
- progressSlider.style.width = '0';
- progressSlider.style.transition = 'width 0.2s';
- progressSlider.style.backgroundColor = 'green';
- this.htmlProgressSliderRef = progressSlider;
- progressBar.append(this.htmlProgressSliderRef);
- const progressLabel = document.createElement('div');
- progressLabel.style.position = 'absolute';
- progressLabel.style.margin = 'auto 0';
- progressLabel.style.width = '100%';
- progressLabel.style.color = 'var(--drag-text)';
- progressLabel.style.fontSize = '14px';
- progressLabel.innerHTML = '0%';
- this.htmlProgressLabelRef = progressLabel;
- progressBar.append(this.htmlProgressLabelRef);
- this.showProgressBar(app.ui.settings.getSettingValue(this.idShowProgressBar, this.defaultShowStatus));
- this.registerListeners();
}
}
const crystoolsProgressBar = new CrystoolsProgressBar();
app.registerExtension({
name: crystoolsProgressBar.idExtensionName,
- setup: crystoolsProgressBar.setup.bind(crystoolsProgressBar),
+ setup: crystoolsProgressBar.setup,
});
diff --git a/web/progressBar.ts b/web/progressBar.ts
index 4fe5e1e..4a8080f 100644
--- a/web/progressBar.ts
+++ b/web/progressBar.ts
@@ -1,158 +1,95 @@
-import { api } from '/scripts/api.js';
-import { app } from '/scripts/app.js';
+import { app, api } from './comfy/index.js';
import { commonPrefix } from './common.js';
-
-enum EStatus {
- executing = 'Executing',
- executed = 'Executed',
- execution_error = 'Execution error',
-}
+import { ProgressBarUI } from './progressBarUI.js';
+import { ComfyKeyMenuDisplayOption, EStatus, MenuDisplayOptions } from './progressBarUIBase.js';
class CrystoolsProgressBar {
idExtensionName = 'Crystools.progressBar';
- idShowProgressBar = 'Crystools.showStatus';
+ idShowProgressBar = 'Crystools.ProgressBar';
defaultShowStatus = true;
menuPrefix = commonPrefix;
- htmlIdCrystoolsRoot = 'crystools-root';
- htmlIdCrystoolsProgressBarContainer = 'crystools-progress-bar-container';
+ menuDisplayOption: MenuDisplayOptions = MenuDisplayOptions.Disabled;
currentStatus = EStatus.executed;
currentProgress = 0;
currentNode?: number = undefined;
timeStart = 0;
- htmlProgressSliderRef?: HTMLDivElement = undefined;
- htmlProgressLabelRef?: HTMLDivElement = undefined;
-
- constructor() {
- this.createSettings();
- }
+ progressBarUI: ProgressBarUI;
// not on setup because this affect the order on settings, I prefer to options at first
createSettings = (): void => {
app.ui.settings.addSetting({
id: this.idShowProgressBar,
- name: this.menuPrefix + '[monitor] Show progress bar',
+ name: 'Show progress bar',
+ category: ['Crystools', this.menuPrefix + ' Progress Bar', 'Show'],
+ tooltip: 'This apply only on "Disabled" (old) menu',
type: 'boolean',
defaultValue: this.defaultShowStatus,
- onChange: this.showProgressBar,
+ onChange: this.progressBarUI.showProgressBar,
});
};
- showProgressBar = (value: boolean): void => {
- const container = document.getElementById(this.htmlIdCrystoolsProgressBarContainer);
-
- // validation because this run before setup
- if (container) {
- container.style.display = value ? 'block' : 'none';
+ updateDisplay = (menuDisplayOption: MenuDisplayOptions): void => {
+ if (menuDisplayOption !== this.menuDisplayOption) {
+ this.menuDisplayOption = menuDisplayOption;
+ this.progressBarUI.showSection(this.menuDisplayOption === MenuDisplayOptions.Disabled);
+ }
+ if (this.menuDisplayOption === MenuDisplayOptions.Disabled && this.progressBarUI.showProgressBarFlag) {
+ console.log('updateDisplay progress bar');
+ this.progressBarUI.updateDisplay(this.currentStatus, this.timeStart, this.currentProgress);
}
};
- updateDisplay = (): void => {
- if (!(this.htmlProgressLabelRef && this.htmlProgressSliderRef)) {
- console.error('htmlProgressLabelRef or htmlProgressSliderRef is undefined');
+ // automatically called by ComfyUI
+ setup = (): void => {
+ if (this.progressBarUI) {
+ this.progressBarUI
+ .showProgressBar(app.ui.settings.getSettingValue(this.idShowProgressBar, this.defaultShowStatus));
return;
}
- if (this.currentStatus === EStatus.executed) {
- // finished
- this.htmlProgressLabelRef.innerHTML = 'cached';
+ this.menuDisplayOption = app.ui.settings.getSettingValue(ComfyKeyMenuDisplayOption, MenuDisplayOptions.Disabled);
+ app.ui.settings.addEventListener(`${ComfyKeyMenuDisplayOption}.change`, (e: any) => {
+ console.log('Comfy.UseNewMenu.change desde progress bar', e.detail.value);
+ this.updateDisplay(e.detail.value);
+ },
+ );
- const timeElapsed = Date.now() - this.timeStart;
- if (this.timeStart > 0 && timeElapsed > 0) {
- this.htmlProgressLabelRef.innerHTML = new Date(timeElapsed).toISOString().substr(11, 8);
- }
- this.htmlProgressSliderRef.style.width = '0';
+ const progressBarElement = document.createElement('div');
+ progressBarElement.classList.add('crystools-monitors-container');
- } else if (this.currentStatus === EStatus.execution_error) {
- // an error occurred
- this.htmlProgressLabelRef.innerHTML = 'ERROR';
- this.htmlProgressSliderRef.style.backgroundColor = 'var(--error-text)';
+ this.progressBarUI = new ProgressBarUI(
+ progressBarElement,
+ (this.menuDisplayOption === MenuDisplayOptions.Disabled),
+ this.centerNode,
+ );
- } else if (this.currentStatus === EStatus.executing) {
- // on going
- this.htmlProgressLabelRef.innerHTML = `${this.currentProgress}%`;
- this.htmlProgressSliderRef.style.width = this.htmlProgressLabelRef.innerHTML;
- this.htmlProgressSliderRef.style.backgroundColor = 'green'; // by reset the color
- }
- };
-
- setup(): void {
const parentElement = document.getElementById('queue-button');
- if (!parentElement) {
- console.error('queue-button not found');
- return;
+ if (parentElement) {
+ parentElement.insertAdjacentElement('afterend', progressBarElement);
+ } else {
+ console.error('Crystools: parentElement to move monitors not found!', parentElement);
}
- let ctoolsRoot = document.getElementById(this.htmlIdCrystoolsRoot);
- if (!ctoolsRoot) {
- ctoolsRoot = document.createElement('div');
- ctoolsRoot.setAttribute('id', this.htmlIdCrystoolsRoot);
- ctoolsRoot.style.display = 'flex';
- ctoolsRoot.style.width = '100%';
- ctoolsRoot.style.flexDirection = 'column';
- parentElement.insertAdjacentElement('afterend', ctoolsRoot);
- }
-
- const htmlContainer = document.createElement('div');
- htmlContainer.setAttribute('id', this.htmlIdCrystoolsProgressBarContainer);
- htmlContainer.setAttribute('title', 'click to see the current working node');
- htmlContainer.style.margin = '4px 0';
- htmlContainer.style.width = '100%';
- htmlContainer.style.cursor = 'pointer';
- htmlContainer.style.order = '1';
- htmlContainer.addEventListener('click', this.centerNode);
- ctoolsRoot.append(htmlContainer);
-
- const progressBar = document.createElement('div');
- progressBar.style.margin = '0 10px';
- progressBar.style.height = '18px';
- progressBar.style.position = 'relative';
- progressBar.style.backgroundColor = 'var(--bg-color)';
- htmlContainer.append(progressBar);
-
- const progressSlider = document.createElement('div');
- progressSlider.style.position = 'absolute';
- progressSlider.style.height = '100%';
- progressSlider.style.width = '0';
- progressSlider.style.transition = 'width 0.2s';
- progressSlider.style.backgroundColor = 'green';
- this.htmlProgressSliderRef = progressSlider;
- progressBar.append(this.htmlProgressSliderRef);
-
- const progressLabel = document.createElement('div');
- progressLabel.style.position = 'absolute';
- progressLabel.style.margin = 'auto 0';
- progressLabel.style.width = '100%';
- progressLabel.style.color = 'var(--drag-text)';
- progressLabel.style.fontSize = '14px';
- progressLabel.innerHTML = '0%';
- this.htmlProgressLabelRef = progressLabel;
- progressBar.append(this.htmlProgressLabelRef);
-
- this.showProgressBar(app.ui.settings.getSettingValue(this.idShowProgressBar, this.defaultShowStatus));
+ this.createSettings();
+ this.updateDisplay(this.menuDisplayOption);
this.registerListeners();
- }
+ };
registerListeners = (): void => {
- api.addEventListener('status', ({
- detail,
- }: any) => {
+ api.addEventListener('status', ({detail}: any) => {
this.currentStatus = this.currentStatus === EStatus.execution_error ? EStatus.execution_error : EStatus.executed;
const queueRemaining = detail?.exec_info.queue_remaining;
if (queueRemaining) {
this.currentStatus = EStatus.executing;
}
- this.updateDisplay();
+ this.updateDisplay(this.menuDisplayOption);
}, false);
- api.addEventListener('progress', ({
- detail,
- }: any) => {
- const {
- value, max, node,
- } = detail;
+ api.addEventListener('progress', ({detail}: any) => {
+ const {value, max, node} = detail;
const progress = Math.floor((value / max) * 100);
if (!isNaN(progress) && progress >= 0 && progress <= 100) {
@@ -160,34 +97,28 @@ class CrystoolsProgressBar {
this.currentNode = node;
}
- this.updateDisplay();
+ this.updateDisplay(this.menuDisplayOption);
}, false);
- api.addEventListener('executed', ({
- detail,
- }: any) => {
+ api.addEventListener('executed', ({detail}: any) => {
if (detail?.node) {
this.currentNode = detail.node;
}
- this.updateDisplay();
+ this.updateDisplay(this.menuDisplayOption);
}, false);
- api.addEventListener('execution_start', ({
- _detail,
- }: any) => {
+ api.addEventListener('execution_start', ({_detail}: any) => {
this.currentStatus = EStatus.executing;
this.timeStart = Date.now();
- this.updateDisplay();
+ this.updateDisplay(this.menuDisplayOption);
}, false);
- api.addEventListener('execution_error', ({
- _detail,
- }: any) => {
+ api.addEventListener('execution_error', ({_detail}: any) => {
this.currentStatus = EStatus.execution_error;
- this.updateDisplay();
+ this.updateDisplay(this.menuDisplayOption);
}, false);
};
@@ -207,5 +138,5 @@ class CrystoolsProgressBar {
const crystoolsProgressBar = new CrystoolsProgressBar();
app.registerExtension({
name: crystoolsProgressBar.idExtensionName,
- setup: crystoolsProgressBar.setup.bind(crystoolsProgressBar),
+ setup: crystoolsProgressBar.setup,
});
diff --git a/web/progressBarUI.d.ts b/web/progressBarUI.d.ts
new file mode 100644
index 0000000..e1bcf11
--- /dev/null
+++ b/web/progressBarUI.d.ts
@@ -0,0 +1,18 @@
+import { EStatus, ProgressBarUIBase } from './progressBarUIBase.js';
+export declare class ProgressBarUI extends ProgressBarUIBase {
+ rootElement: HTMLElement;
+ showSectionFlag: boolean;
+ private centerNode;
+ htmlProgressSliderRef: HTMLDivElement;
+ htmlProgressLabelRef: HTMLDivElement;
+ currentStatus: EStatus;
+ timeStart: number;
+ currentProgress: number;
+ showProgressBarFlag: boolean;
+ constructor(rootElement: HTMLElement, showSectionFlag: boolean, centerNode: () => void);
+ createDOM: () => void;
+ updateDisplay: (currentStatus: EStatus, timeStart: number, currentProgress: number) => void;
+ showSection: (value: boolean) => void;
+ showProgressBar: (value: boolean) => void;
+ private displaySection;
+}
diff --git a/web/progressBarUI.js b/web/progressBarUI.js
new file mode 100644
index 0000000..ed1d4fb
--- /dev/null
+++ b/web/progressBarUI.js
@@ -0,0 +1,142 @@
+import { EStatus, ProgressBarUIBase } from './progressBarUIBase.js';
+export class ProgressBarUI extends ProgressBarUIBase {
+ constructor(rootElement, showSectionFlag, centerNode) {
+ super('crystools-progressBar-root', rootElement);
+ Object.defineProperty(this, "rootElement", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: rootElement
+ });
+ Object.defineProperty(this, "showSectionFlag", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: showSectionFlag
+ });
+ Object.defineProperty(this, "centerNode", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: centerNode
+ });
+ Object.defineProperty(this, "htmlProgressSliderRef", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: void 0
+ });
+ Object.defineProperty(this, "htmlProgressLabelRef", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: void 0
+ });
+ Object.defineProperty(this, "currentStatus", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: void 0
+ });
+ Object.defineProperty(this, "timeStart", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: void 0
+ });
+ Object.defineProperty(this, "currentProgress", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: void 0
+ });
+ Object.defineProperty(this, "showProgressBarFlag", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: void 0
+ });
+ Object.defineProperty(this, "createDOM", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: () => {
+ this.rootElement.setAttribute('title', 'click to see the current working node');
+ this.rootElement.addEventListener('click', this.centerNode);
+ const progressBar = document.createElement('div');
+ progressBar.classList.add('crystools-progress-bar');
+ this.rootElement.append(progressBar);
+ const progressSlider = document.createElement('div');
+ this.htmlProgressSliderRef = progressSlider;
+ progressSlider.classList.add('crystools-slider');
+ progressBar.append(this.htmlProgressSliderRef);
+ const progressLabel = document.createElement('div');
+ progressLabel.classList.add('crystools-label');
+ progressLabel.innerHTML = '0%';
+ this.htmlProgressLabelRef = progressLabel;
+ progressBar.append(this.htmlProgressLabelRef);
+ }
+ });
+ Object.defineProperty(this, "updateDisplay", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: (currentStatus, timeStart, currentProgress) => {
+ if (!(this.showSectionFlag && this.showProgressBarFlag)) {
+ return;
+ }
+ if (!(this.htmlProgressLabelRef && this.htmlProgressSliderRef)) {
+ console.error('htmlProgressLabelRef or htmlProgressSliderRef is undefined');
+ return;
+ }
+ this.currentStatus = currentStatus;
+ this.timeStart = timeStart;
+ this.currentProgress = currentProgress;
+ if (currentStatus === EStatus.executed) {
+ this.htmlProgressLabelRef.innerHTML = 'cached';
+ const timeElapsed = Date.now() - timeStart;
+ if (timeStart > 0 && timeElapsed > 0) {
+ this.htmlProgressLabelRef.innerHTML = new Date(timeElapsed).toISOString().substr(11, 8);
+ }
+ this.htmlProgressSliderRef.style.width = '0';
+ }
+ else if (currentStatus === EStatus.execution_error) {
+ this.htmlProgressLabelRef.innerHTML = 'ERROR';
+ this.htmlProgressSliderRef.style.backgroundColor = 'var(--error-text)';
+ }
+ else if (currentStatus === EStatus.executing) {
+ this.htmlProgressLabelRef.innerHTML = `${currentProgress}%`;
+ this.htmlProgressSliderRef.style.width = this.htmlProgressLabelRef.innerHTML;
+ this.htmlProgressSliderRef.style.backgroundColor = 'green';
+ }
+ }
+ });
+ Object.defineProperty(this, "showSection", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: (value) => {
+ this.showSectionFlag = value;
+ this.displaySection();
+ }
+ });
+ Object.defineProperty(this, "showProgressBar", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: (value) => {
+ this.showProgressBarFlag = value;
+ this.displaySection();
+ }
+ });
+ Object.defineProperty(this, "displaySection", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: () => {
+ this.rootElement.style.display = (this.showSectionFlag && this.showProgressBarFlag) ? 'block' : 'none';
+ }
+ });
+ this.createDOM();
+ }
+}
diff --git a/web/progressBarUI.ts b/web/progressBarUI.ts
new file mode 100644
index 0000000..218d53b
--- /dev/null
+++ b/web/progressBarUI.ts
@@ -0,0 +1,95 @@
+import { EStatus, ProgressBarUIBase } from './progressBarUIBase.js';
+
+export class ProgressBarUI extends ProgressBarUIBase {
+ htmlProgressSliderRef: HTMLDivElement;
+ htmlProgressLabelRef: HTMLDivElement;
+ currentStatus: EStatus;
+ timeStart: number;
+ currentProgress: number;
+ showProgressBarFlag: boolean;
+
+ constructor(
+ public override rootElement: HTMLElement,
+ public showSectionFlag: boolean,
+ private centerNode: () => void,
+ ) {
+ super('crystools-progressBar-root', rootElement);
+ this.createDOM();
+ }
+
+ createDOM = (): void => {
+ this.rootElement.setAttribute('title', 'click to see the current working node');
+ this.rootElement.addEventListener('click', this.centerNode);
+
+ const progressBar = document.createElement('div');
+ progressBar.classList.add('crystools-progress-bar');
+ this.rootElement.append(progressBar);
+
+ const progressSlider = document.createElement('div');
+ this.htmlProgressSliderRef = progressSlider;
+ progressSlider.classList.add('crystools-slider');
+ progressBar.append(this.htmlProgressSliderRef);
+
+ const progressLabel = document.createElement('div');
+ progressLabel.classList.add('crystools-label');
+ progressLabel.innerHTML = '0%';
+ this.htmlProgressLabelRef = progressLabel;
+ progressBar.append(this.htmlProgressLabelRef);
+ };
+
+ // eslint-disable-next-line complexity
+ updateDisplay = (currentStatus: EStatus, timeStart: number, currentProgress: number): void => {
+ if (!(this.showSectionFlag && this.showProgressBarFlag)) {
+ return;
+ }
+
+ if (!(this.htmlProgressLabelRef && this.htmlProgressSliderRef)) {
+ console.error('htmlProgressLabelRef or htmlProgressSliderRef is undefined');
+ return;
+ }
+
+ // console.log('only if showSection and progressBar', timeStart, currentProgress);
+
+ this.currentStatus = currentStatus;
+ this.timeStart = timeStart;
+ this.currentProgress = currentProgress;
+
+ if (currentStatus === EStatus.executed) {
+ // finished
+ this.htmlProgressLabelRef.innerHTML = 'cached';
+
+ const timeElapsed = Date.now() - timeStart;
+ if (timeStart > 0 && timeElapsed > 0) {
+ this.htmlProgressLabelRef.innerHTML = new Date(timeElapsed).toISOString().substr(11, 8);
+ }
+ this.htmlProgressSliderRef.style.width = '0';
+
+ } else if (currentStatus === EStatus.execution_error) {
+ // an error occurred
+ this.htmlProgressLabelRef.innerHTML = 'ERROR';
+ this.htmlProgressSliderRef.style.backgroundColor = 'var(--error-text)';
+
+ } else if (currentStatus === EStatus.executing) {
+ // on going
+ this.htmlProgressLabelRef.innerHTML = `${currentProgress}%`;
+ this.htmlProgressSliderRef.style.width = this.htmlProgressLabelRef.innerHTML;
+ this.htmlProgressSliderRef.style.backgroundColor = 'green'; // by reset the color
+ }
+
+ };
+
+ public showSection = (value: boolean): void => {
+ this.showSectionFlag = value;
+ this.displaySection();
+ };
+
+ // remember it can't have more parameters because it is used on settings automatically
+ public showProgressBar = (value: boolean): void => {
+ this.showProgressBarFlag = value;
+ this.displaySection();
+ };
+
+ private displaySection = (): void => {
+ this.rootElement.style.display = (this.showSectionFlag && this.showProgressBarFlag) ? 'block' : 'none';
+ };
+}
diff --git a/web/progressBarUIBase.d.ts b/web/progressBarUIBase.d.ts
new file mode 100644
index 0000000..dace402
--- /dev/null
+++ b/web/progressBarUIBase.d.ts
@@ -0,0 +1,18 @@
+export declare enum EStatus {
+ executing = "Executing",
+ executed = "Executed",
+ execution_error = "Execution error"
+}
+export declare const ComfyKeyMenuDisplayOption = "Comfy.UseNewMenu";
+export declare enum MenuDisplayOptions {
+ 'Disabled' = "Disabled",
+ 'Top' = "Top",
+ 'Bottom' = "Bottom"
+}
+export declare abstract class ProgressBarUIBase {
+ rootId: string;
+ rootElement: HTMLElement | null | undefined;
+ protected htmlClassMonitor: string;
+ protected constructor(rootId: string, rootElement: HTMLElement | null | undefined);
+ abstract createDOM(): void;
+}
diff --git a/web/progressBarUIBase.js b/web/progressBarUIBase.js
new file mode 100644
index 0000000..c9fb174
--- /dev/null
+++ b/web/progressBarUIBase.js
@@ -0,0 +1,42 @@
+export var EStatus;
+(function (EStatus) {
+ EStatus["executing"] = "Executing";
+ EStatus["executed"] = "Executed";
+ EStatus["execution_error"] = "Execution error";
+})(EStatus || (EStatus = {}));
+export const ComfyKeyMenuDisplayOption = 'Comfy.UseNewMenu';
+export var MenuDisplayOptions;
+(function (MenuDisplayOptions) {
+ MenuDisplayOptions["Disabled"] = "Disabled";
+ MenuDisplayOptions["Top"] = "Top";
+ MenuDisplayOptions["Bottom"] = "Bottom";
+})(MenuDisplayOptions || (MenuDisplayOptions = {}));
+export class ProgressBarUIBase {
+ constructor(rootId, rootElement) {
+ Object.defineProperty(this, "rootId", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: rootId
+ });
+ Object.defineProperty(this, "rootElement", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: rootElement
+ });
+ Object.defineProperty(this, "htmlClassMonitor", {
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ value: 'crystools-monitors-container'
+ });
+ if (this.rootElement && this.rootElement.children.length === 0) {
+ this.rootElement.setAttribute('id', this.rootId);
+ this.rootElement.classList.add(this.htmlClassMonitor);
+ this.rootElement.classList.add(this.constructor.name);
+ }
+ else {
+ }
+ }
+}
diff --git a/web/progressBarUIBase.ts b/web/progressBarUIBase.ts
new file mode 100644
index 0000000..9b9486b
--- /dev/null
+++ b/web/progressBarUIBase.ts
@@ -0,0 +1,32 @@
+export enum EStatus {
+ executing = 'Executing',
+ executed = 'Executed',
+ execution_error = 'Execution error',
+}
+
+export const ComfyKeyMenuDisplayOption = 'Comfy.UseNewMenu';
+export enum MenuDisplayOptions {
+ 'Disabled' = 'Disabled',
+ 'Top' = 'Top',
+ 'Bottom' = 'Bottom',
+}
+
+export abstract class ProgressBarUIBase {
+ protected htmlClassMonitor = 'crystools-monitors-container';
+
+ protected constructor(
+ public rootId: string,
+ public rootElement: HTMLElement | null | undefined,
+ ) {
+ // IMPORTANT duplicate on crystools-save
+ if (this.rootElement && this.rootElement.children.length === 0) {
+ this.rootElement.setAttribute('id', this.rootId);
+ this.rootElement.classList.add(this.htmlClassMonitor);
+ this.rootElement.classList.add(this.constructor.name);
+ } else {
+ // it was created before
+ }
+ }
+
+ abstract createDOM(): void;
+}
diff --git a/web/styles.d.ts b/web/styles.d.ts
new file mode 100644
index 0000000..4d8a4db
--- /dev/null
+++ b/web/styles.d.ts
@@ -0,0 +1,12 @@
+export declare enum Styles {
+ 'BARS' = "BARS"
+}
+export declare enum Colors {
+ 'CPU' = "#0AA015",
+ 'RAM' = "#07630D",
+ 'DISK' = "#730F92",
+ 'GPU' = "#0C86F4",
+ 'VRAM' = "#176EC7",
+ 'TEMP_START' = "#00ff00",
+ 'TEMP_END' = "#ff0000"
+}
diff --git a/web/styles.js b/web/styles.js
new file mode 100644
index 0000000..843268d
--- /dev/null
+++ b/web/styles.js
@@ -0,0 +1,16 @@
+import { utils } from './comfy/index.js';
+utils.addStylesheet('extensions/ComfyUI-Crystools/monitor.css');
+export var Styles;
+(function (Styles) {
+ Styles["BARS"] = "BARS";
+})(Styles || (Styles = {}));
+export var Colors;
+(function (Colors) {
+ Colors["CPU"] = "#0AA015";
+ Colors["RAM"] = "#07630D";
+ Colors["DISK"] = "#730F92";
+ Colors["GPU"] = "#0C86F4";
+ Colors["VRAM"] = "#176EC7";
+ Colors["TEMP_START"] = "#00ff00";
+ Colors["TEMP_END"] = "#ff0000";
+})(Colors || (Colors = {}));
diff --git a/web/styles.ts b/web/styles.ts
new file mode 100644
index 0000000..3426fb9
--- /dev/null
+++ b/web/styles.ts
@@ -0,0 +1,17 @@
+import { utils } from './comfy/index.js';
+
+utils.addStylesheet('extensions/ComfyUI-Crystools/monitor.css');
+
+export enum Styles {
+ 'BARS' = 'BARS'
+}
+
+export enum Colors {
+ 'CPU' = '#0AA015',
+ 'RAM' = '#07630D',
+ 'DISK' = '#730F92',
+ 'GPU' = '#0C86F4',
+ 'VRAM' = '#176EC7',
+ 'TEMP_START' = '#00ff00',
+ 'TEMP_END' = '#ff0000',
+}
diff --git a/web/monitor.types.d.ts b/web/types.d.ts
similarity index 79%
rename from web/monitor.types.d.ts
rename to web/types.d.ts
index 77d96cf..3480acf 100644
--- a/web/monitor.types.d.ts
+++ b/web/types.d.ts
@@ -1,3 +1,4 @@
+
type TGpuStatData = {
gpu_utilization: number,
gpu_temperature: number,
@@ -42,13 +43,16 @@ type TStatsSettings = {
type TMonitorSettings = {
id: string,
name: string,
+ category: string[], // for settings location
label: string, // on monitor
symbol: string, // on monitor
+ monitorTitle?: string, // on monitor
title?: string, // on monitor
tooltip?: string, // on settings
- type: 'boolean' | 'number' | 'string',
- defaultValue: boolean,
- onChange: (value: boolean) => Promise,
+ type: 'boolean' | 'number' | 'string' | 'slider' | 'combo',
+ defaultValue: boolean | number | string,
+ data?: any,
+ onChange: (value: boolean | number | string) => Promise,
htmlMonitorRef?: HTMLDivElement,
htmlMonitorSliderRef?: HTMLDivElement,
htmlMonitorLabelRef?: HTMLDivElement,
diff --git a/web/utils.d.ts b/web/utils.d.ts
new file mode 100644
index 0000000..31bb65a
--- /dev/null
+++ b/web/utils.d.ts
@@ -0,0 +1,5 @@
+export declare function numberToWords(num: number): string;
+export declare function toPascalCase(strings: string[]): string;
+export declare function convertNumberToPascalCase(num: number): string;
+export declare function formatBytes(bytes: number): string;
+export declare function createStyleSheet(id: string): HTMLStyleElement;
diff --git a/web/utils.js b/web/utils.js
new file mode 100644
index 0000000..54362dd
--- /dev/null
+++ b/web/utils.js
@@ -0,0 +1,56 @@
+export function numberToWords(num) {
+ if (num === 0)
+ return 'zero';
+ const belowTwenty = [
+ '', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten',
+ 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen', 'nineteen'
+ ];
+ const tens = ['', '', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety'];
+ const thousands = ['', 'thousand', 'million', 'billion'];
+ function helper(n) {
+ if (n === 0)
+ return '';
+ else if (n < 20)
+ return belowTwenty[n] + ' ';
+ else if (n < 100)
+ return tens[Math.floor(n / 10)] + ' ' + helper(n % 10);
+ return belowTwenty[Math.floor(n / 100)] + ' hundred ' + helper(n % 100);
+ }
+ let word = '';
+ let i = 0;
+ while (num > 0) {
+ if (num % 1000 !== 0) {
+ word = helper(num % 1000) + thousands[i] + ' ' + word;
+ }
+ num = Math.floor(num / 1000);
+ i++;
+ }
+ return word.trim();
+}
+export function toPascalCase(strings) {
+ if (!Array.isArray(strings))
+ return '';
+ function capitalize(word) {
+ return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
+ }
+ return strings.map(capitalize).join('');
+}
+export function convertNumberToPascalCase(num) {
+ return toPascalCase(numberToWords(num).split(' '));
+}
+export function formatBytes(bytes) {
+ if (bytes === 0)
+ return '0 Bytes';
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
+ const formattedSize = (bytes / Math.pow(1024, i)).toFixed(2);
+ return `${formattedSize} ${sizes[i]}`;
+}
+export function createStyleSheet(id) {
+ const style = document.createElement('style');
+ style.setAttribute('id', id);
+ style.setAttribute('rel', 'stylesheet');
+ style.setAttribute('type', 'text/css');
+ document.head.appendChild(style);
+ return style;
+}
diff --git a/web/utils.ts b/web/utils.ts
new file mode 100644
index 0000000..f6930ca
--- /dev/null
+++ b/web/utils.ts
@@ -0,0 +1,64 @@
+export function numberToWords(num: number): string {
+ if (num === 0) return 'zero';
+
+ const belowTwenty = [
+ '', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten',
+ 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen', 'nineteen'
+ ];
+ const tens = ['', '', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety'];
+ const thousands = ['', 'thousand', 'million', 'billion'];
+
+ function helper(n: number): string {
+ if (n === 0) return '';
+ else if (n < 20) return belowTwenty[n] + ' ';
+ else if (n < 100) return tens[Math.floor(n / 10)] + ' ' + helper(n % 10);
+ return belowTwenty[Math.floor(n / 100)] + ' hundred ' + helper(n % 100);
+ }
+
+ let word = '';
+ let i = 0;
+
+ while (num > 0) {
+ if (num % 1000 !== 0) {
+ word = helper(num % 1000) + thousands[i] + ' ' + word;
+ }
+ num = Math.floor(num / 1000);
+ i++;
+ }
+
+ return word.trim();
+}
+
+export function toPascalCase(strings: string[]): string {
+ if (!Array.isArray(strings)) return '';
+
+ function capitalize(word: string): string {
+ return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
+ }
+
+ return strings.map(capitalize).join('');
+}
+
+
+export function convertNumberToPascalCase(num: number): string {
+ return toPascalCase(numberToWords(num).split(' '));
+}
+
+export function formatBytes(bytes: number): string {
+ if (bytes === 0) return '0 Bytes';
+
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
+ const formattedSize = (bytes / Math.pow(1024, i)).toFixed(2);
+
+ return `${formattedSize} ${sizes[i]}`;
+}
+
+export function createStyleSheet(id: string): HTMLStyleElement {
+ const style = document.createElement('style');
+ style.setAttribute('id', id);
+ style.setAttribute('rel', 'stylesheet');
+ style.setAttribute('type', 'text/css');
+ document.head.appendChild(style);
+ return style;
+}