Add custom nodes, Civitai loras (LFS), and vast.ai setup script
Some checks failed
Python Linting / Run Ruff (push) Has been cancelled
Python Linting / Run Pylint (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Has been cancelled
Execution Tests / test (macos-latest) (push) Has been cancelled
Execution Tests / test (ubuntu-latest) (push) Has been cancelled
Execution Tests / test (windows-latest) (push) Has been cancelled
Test server launches without errors / test (push) Has been cancelled
Unit Tests / test (macos-latest) (push) Has been cancelled
Unit Tests / test (ubuntu-latest) (push) Has been cancelled
Unit Tests / test (windows-2022) (push) Has been cancelled

Includes 30 custom nodes committed directly, 7 Civitai-exclusive
loras stored via Git LFS, and a setup script that installs all
dependencies and downloads HuggingFace-hosted models on vast.ai.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-09 00:55:26 +00:00
parent 2b70ab9ad0
commit f09734b0ee
2274 changed files with 748556 additions and 3 deletions

View File

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

View File

@@ -0,0 +1,2 @@
export * from './scripts.js';
export * from './liteGraph.js';

View File

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

View File

@@ -0,0 +1,19 @@
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;
serializeValue?: () => Promise<void>;
}
export declare class TLGraphNode extends LGraphNode {
static category: string;
static shape: number;
static color: string;
static bgcolor: string;
static collapsable: boolean;
isVirtualNode?: boolean;
widgets_values?: any[];
name?: string;
prototype: TLGraphNode;
}
export declare const LiteGraph: TypeLiteGraph;

View File

@@ -0,0 +1,30 @@
export class TLGraphNode extends LGraphNode {
constructor() {
super(...arguments);
Object.defineProperty(this, "isVirtualNode", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "widgets_values", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "name", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "prototype", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
}
}
export const LiteGraph = window.LiteGraph;

View File

@@ -0,0 +1,32 @@
// / <reference path="/types/litegraph.d.ts" />
// A LOTS OF PATCHES FOR 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; // just for get the type
export interface IWidget extends IWidgetOld {
onRemove?: () => void;
serializeValue?: () => Promise<void>;
}
export class TLGraphNode extends LGraphNode {
// on discovery...
static category: string;
static shape: number;
static color: string;
static bgcolor: string;
static collapsable: boolean;
// widgets?: IWidget[];
isVirtualNode?: boolean;
// override onResize?: (size: [number, number]) => void;
widgets_values?: any[];
name?: string;
prototype: TLGraphNode; // yes itself
}
// from globals
export const LiteGraph: TypeLiteGraph = (window as any).LiteGraph;

View File

@@ -0,0 +1,19 @@
// A LOTS OF PATCHES FOR LITEGRAPH TYPES ¯\_(ツ)_/¯
import { LGraph } from './typings/litegraph.js';
export type * from './typings/litegraph.js';
export declare type TypeLiteGraph = typeof LiteGraph & {
graph: LGraph;
};
// export declare type ComfyApi = typeof api;
export declare type ComfyNode = any;
// I prefer not use global, but if I change of opinion:
// / <reference path="./types.ts" />
// import { LGraphNode as TLGraphNode, LiteGraph as TLiteGraph } from '/types/litegraph';
// export declare const LGraphNode: typeof TLGraphNode;
// declare global {
// const LGraphNode: LGraphNode;
// }

View File

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

View File

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

View File

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

View File

@@ -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 '/// &lt;reference>' comment should have picked up the types, but it wasn't having it. ¯\_(ツ)_/¯

View File

@@ -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<void>;
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<void>;
// workflow: ComfyWorkflowInstance ???
async loadGraphData(graphData: {}, clean?: boolean, restore_view?: boolean, workflow?: any|null): Promise<void>
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<TValue>;
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<ComfyNode> {
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<void>;
/**
* Allows any additonal setup, called after the application is fully set up and running
* @param app The ComfyUI app instance
*/
setup?(app: ComfyApp): Promise<void>;
/**
* 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<string, ComfyObjectInfo>, app: ComfyApp): Promise<void>;
/**
* 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<string, (node, inputName, inputData, app) => { 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<void>;
/**
* 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<void>;
/**
* 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<string, ComfyObjectInfoConfig>;
optional?: Record<string, ComfyObjectInfoConfig>;
hidden?: Record<string, ComfyObjectInfoConfig>;
};
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[])};
}

View File

@@ -0,0 +1,55 @@
import { LGraph } from "./litegraph.js";
export type Constructor<T> = 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<number | string>;
}
export interface SerializedGraph {
config: any;
extra: any;
groups: any;
last_link_id: number;
last_node_id: number;
links: SerializedLink[];
nodes: SerializedNode[];
}
export interface BadLinksData<T = SerializedGraph|LGraph> {
hasBadLinks: boolean;
fixed: boolean;
graph: T;
patched: number;
deleted: number;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,4 @@
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;

View File

@@ -0,0 +1,50 @@
import { ComfyWidgets } from './comfy/index.js';
export const commonPrefix = '🪛';
export function displayContext(nodeType, appFromArg, index = 0, serialize_widgets = false, isVirtualNode = false) {
function populate(text) {
if (this.widgets) {
const pos = this.widgets.findIndex((w) => w.name === 'text');
if (pos !== -1) {
for (let i = pos; i < this.widgets.length; i++) {
this.widgets[i]?.onRemove?.();
}
this.widgets.length = pos;
}
}
this.serialize_widgets = serialize_widgets;
this.isVirtualNode = isVirtualNode;
const widget = ComfyWidgets.STRING(this, 'text', [
'STRING', { multiline: true },
], appFromArg).widget;
widget.inputEl.readOnly = true;
widget.inputEl.style.opacity = 0.6;
if (Array.isArray(text) && index !== undefined && text[index] !== undefined) {
text = text[index];
}
widget.value = text || '';
widget.serializeValue = async () => { };
requestAnimationFrame(() => {
const sz = this.computeSize();
if (sz[0] < this.size[0]) {
sz[0] = this.size[0];
}
if (sz[1] < this.size[1]) {
sz[1] = this.size[1];
}
this.onResize?.(sz);
appFromArg.graph.setDirtyCanvas(true, false);
});
}
const onExecutedOriginal = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
onExecutedOriginal?.apply(this, arguments);
populate.call(this, message.text);
};
const onConfigureOriginal = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function () {
onConfigureOriginal?.apply(this, arguments);
if (this.widgets_values?.length) {
populate.call(this, this.widgets_values);
}
};
}

View File

@@ -0,0 +1,92 @@
import type { TLGraphNode } from './comfy/index.js';
import { ComfyWidgets, ComfyApp } from './comfy/index.js';
export const commonPrefix = '🪛';
export function displayContext(
nodeType: TLGraphNode,
appFromArg: ComfyApp,
index = 0, serialize_widgets = false, isVirtualNode = false,
): void {
function populate(this: TLGraphNode, text: string | string[]): void {
if (this.widgets) {
const pos = this.widgets.findIndex((w) => w.name === 'text');
if (pos !== -1) {
for (let i = pos; i < this.widgets.length; i++) {
this.widgets[i]?.onRemove?.();
}
this.widgets.length = pos;
}
}
// If you want to do not save properties in the node (be careful with F5)
// BUG on isVirtualNode, with "true", it ignores OUTPUT_NODE on py file!
this.serialize_widgets = serialize_widgets;
this.isVirtualNode = isVirtualNode;
const widget = ComfyWidgets.STRING(this, 'text', [
'STRING', { multiline: true },
], appFromArg).widget;
widget.inputEl.readOnly = true;
widget.inputEl.style.opacity = 0.6;
if (Array.isArray(text) && index !== undefined && text[index] !== undefined) {
// @ts-ignore
text = text[index];
}
widget.value = text || '';
// eslint-disable-next-line @typescript-eslint/no-empty-function
widget.serializeValue = async(): Promise<void> => {}; // just for no serialized to itself!
requestAnimationFrame(() => {
const sz = this.computeSize();
if (sz[0] < this.size[0]) {
sz[0] = this.size[0];
}
if (sz[1] < this.size[1]) {
sz[1] = this.size[1];
}
this.onResize?.(sz);
appFromArg.graph.setDirtyCanvas(true, false);
});
}
// 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;
nodeType.prototype.onExecuted = function(message: { text: string }): void {
// @ts-ignore
onExecutedOriginal?.apply(this, arguments);
populate.call(this, message.text);
};
// eslint-disable-next-line @typescript-eslint/unbound-method
const onConfigureOriginal = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function(): void {
// @ts-ignore
onConfigureOriginal?.apply(this, arguments);
if (this.widgets_values?.length) {
populate.call(this, this.widgets_values);
}
};
}
// propagate the output value to the dependents nodes, it does not work with some nodes ¯\_(ツ)_/¯
// const propagateOutputToDependentsNodes = function(output: serializedLGraph, value: string) {
// if (output.links?.length) {
// for (const l of output.links) {
// const link_info = app.graph.links[l];
// const outNode = app.graph.getNodeById(link_info.target_id);
// const outIn = outNode?.inputs?.[link_info.target_slot];
// if (outIn.widget) {
// const widget = outNode.widgets.find((w: IWidget) => w.name === outIn.widget.name);
// if (!widget) {
// continue;
// }
// widget.value = value;
// }
// }
// }
// };

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,77 @@
import { app, api, ComfyWidgets, LiteGraph, TLGraphNode } from './comfy/index.js';
import { commonPrefix, displayContext } from './common.js';
app.registerExtension({
name: 'Crystools.Debugger.ConsoleAny',
beforeRegisterNodeDef(nodeType, nodeData, appFromArg) {
if (nodeData.name === 'Show any [Crystools]') {
displayContext(nodeType, appFromArg, 3);
}
},
});
app.registerExtension({
name: 'Crystools.Debugger.Metadata',
registerCustomNodes() {
class MetadataNode extends TLGraphNode {
constructor() {
super(`${commonPrefix} Show Metadata `);
Object.defineProperty(this, "fillMetadataWidget", {
enumerable: true,
configurable: true,
writable: true,
value: () => {
return app.graphToPrompt()
.then((workflow) => {
let result = 'inactive';
if (this.widgets?.length !== 4) {
console.error('Something is wrong with the widgets, should be 4!');
return 'error';
}
const output = this.widgets[0];
const active = this.widgets[1]?.value;
const parsed = this.widgets[2]?.value;
let what = this.widgets[3]?.value.toLowerCase();
if (active) {
what = what === 'prompt' ? 'output' : what;
result = workflow[what];
if (parsed) {
result = JSON.stringify(result, null, 2);
}
else {
result = JSON.stringify(result);
}
}
if (output) {
output.value = result;
}
else {
console.error('Something is wrong with the widgets, output is undefined!');
return 'error';
}
return result;
});
}
});
this.serialize_widgets = false;
this.isVirtualNode = true;
const widget = ComfyWidgets.STRING(this, '', [
'', { default: '', multiline: true },
], app).widget;
widget.inputEl.readOnly = true;
ComfyWidgets.BOOLEAN(this, 'Active', [
'', { default: true },
]);
ComfyWidgets.BOOLEAN(this, 'Parsed', [
'', { default: true },
]);
ComfyWidgets.COMBO(this, 'What', [
['Prompt', 'Workflow'], { default: 'Prompt' },
]);
api.addEventListener('executed', this.fillMetadataWidget, false);
}
}
LiteGraph.registerNodeType('Show Metadata [Crystools]', MetadataNode);
MetadataNode.category = `crystools ${commonPrefix}/Debugger`;
MetadataNode.shape = LiteGraph.BOX_SHAPE;
MetadataNode.title = `${commonPrefix} Show Metadata`;
},
});

View File

@@ -0,0 +1,86 @@
import type { ComfyNode } from './comfy/index.js';
import { app, api, ComfyWidgets, LiteGraph, TLGraphNode, ComfyApp } from './comfy/index.js';
import { commonPrefix, displayContext } from './common.js';
// "Show any" Node
app.registerExtension({
name: 'Crystools.Debugger.ConsoleAny',
beforeRegisterNodeDef(nodeType: ComfyNode, nodeData: TLGraphNode, appFromArg: ComfyApp) {
if (nodeData.name === 'Show any [Crystools]') {
// 3 is the index of the text field in the node
displayContext(nodeType, appFromArg, 3);
}
},
});
app.registerExtension({
name: 'Crystools.Debugger.Metadata',
registerCustomNodes() {
class MetadataNode extends TLGraphNode {
constructor() {
super(`${commonPrefix} Show Metadata `);
this.serialize_widgets = false;
this.isVirtualNode = true;
const widget = ComfyWidgets.STRING(this, '', [
'', {default: '', multiline: true},
], app).widget;
widget.inputEl.readOnly = true;
ComfyWidgets.BOOLEAN(this, 'Active', [
'', {default: true},
]);
ComfyWidgets.BOOLEAN(this, 'Parsed', [
'', {default: true},
]);
ComfyWidgets.COMBO(this, 'What', [
['Prompt', 'Workflow'], {default: 'Prompt'},
]);
// It runs at finish on each prompt queue
api.addEventListener('executed', this.fillMetadataWidget, false);
}
fillMetadataWidget = (): Promise<string> => {
return app.graphToPrompt()
.then((workflow: any): string => {
let result = 'inactive';
if (this.widgets?.length !== 4) {
console.error('Something is wrong with the widgets, should be 4!');
return 'error';
}
const output = this.widgets[0];
const active = this.widgets[1]?.value;
const parsed = this.widgets[2]?.value;
let what = this.widgets[3]?.value.toLowerCase();
if (active) {
what = what === 'prompt' ? 'output' : what; // little fix for better understanding
// @ts-ignore
result = workflow[what];
if (parsed) {
result = JSON.stringify(result, null, 2);
} else {
result = JSON.stringify(result);
}
}
if (output) {
output.value = result;
} else {
console.error('Something is wrong with the widgets, output is undefined!');
return 'error';
}
return result;
});
};
}
// I'm not sure for what they're using prototype and lots of black magic, don't change the order!
LiteGraph.registerNodeType('Show Metadata [Crystools]', MetadataNode);
MetadataNode.category = `crystools ${commonPrefix}/Debugger`;
MetadataNode.shape = LiteGraph.BOX_SHAPE;
MetadataNode.title = `${commonPrefix} Show Metadata`;
// MetadataNode.collapsable = false;
// MetadataNode.color = '#FF2222';
// MetadataNode.bgcolor = '#000000';
},
});

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,32 @@
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',
'Preview from metadata [Crystools]': 'Crystools.Image.PreviewFromMetadata',
'Metadata comparator [Crystools]': 'Crystools.Metadata.MetadataComparator',
'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) {
if (nodeData.name in crystoolsExtensionsSerialized) {
displayContext(nodeType, appFromArg, 0, true);
}
else {
displayContext(nodeType, appFromArg, 0);
}
}
},
});
});

View File

@@ -0,0 +1,38 @@
import { app, TLGraphNode, ComfyApp } from './comfy/index.js';
import type { ComfyNode } from './comfy/index.js';
import { displayContext } from './common.js';
const crystoolsExtensionsSerialized: Record<string, string> = {
// '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<string, string> = {
'Get resolution [Crystools]': 'Crystools.Image.GetResolution',
'Preview from image [Crystools]': 'Crystools.Image.PreviewFromImage',
'Preview from metadata [Crystools]': 'Crystools.Image.PreviewFromMetadata',
'Metadata comparator [Crystools]': 'Crystools.Metadata.MetadataComparator',
'Stats system [Crystools]': 'Crystools.Utils.StatsSystem',
'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) {
if (nodeData.name in crystoolsExtensionsSerialized) {
displayContext(nodeType, appFromArg, 0, true); // serialize_widgets = true
} else {
displayContext(nodeType, appFromArg, 0);
}
}
},
});
});

View File

@@ -0,0 +1,188 @@
/* Vertical UI */
.comfy-menu, .side-bar-panel {
#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;
}
}
}
/* side-bar-panel */
.side-bar-panel {
#crystools-monitors-root {
width: 230px;
margin-left: auto;
margin-right: auto;
}
}
.comfyui-queue-button {
/*width: unset !important;*/
}
/* Horizontal UI */
#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;
}
}

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,656 @@
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", {
enumerable: true,
configurable: true,
writable: true,
value: 'Crystools.monitor'
});
Object.defineProperty(this, "menuPrefix", {
enumerable: true,
configurable: true,
writable: true,
value: commonPrefix
});
Object.defineProperty(this, "menuDisplayOption", {
enumerable: true,
configurable: true,
writable: true,
value: MenuDisplayOptions.Disabled
});
Object.defineProperty(this, "crystoolsButtonGroup", {
enumerable: true,
configurable: true,
writable: true,
value: null
});
Object.defineProperty(this, "settingsRate", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "settingsMonitorHeight", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "settingsMonitorWidth", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "monitorCPUElement", {
enumerable: true,
configurable: true,
writable: true,
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,
configurable: true,
writable: true,
value: []
});
Object.defineProperty(this, "monitorVRAMSettings", {
enumerable: true,
configurable: true,
writable: true,
value: []
});
Object.defineProperty(this, "monitorTemperatureSettings", {
enumerable: true,
configurable: true,
writable: true,
value: []
});
Object.defineProperty(this, "monitorUI", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "monitorWidthId", {
enumerable: true,
configurable: true,
writable: true,
value: 'Crystools.MonitorWidth'
});
Object.defineProperty(this, "monitorWidth", {
enumerable: true,
configurable: true,
writable: true,
value: 60
});
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: () => {
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: 2,
step: .25,
},
defaultValue: .5,
onChange: async (value) => {
let valueNumber;
try {
valueNumber = parseFloat(value);
if (isNaN(valueNumber)) {
throw new Error('invalid value');
}
}
catch (error) {
console.error(error);
return;
}
try {
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.monitorUI.updateDisplay(data);
}
this.monitorUI?.updateAllAnimationDuration(valueNumber);
},
};
}
});
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 top/bottom 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.extensionManager.setting.get(this.monitorHeightId);
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 top/bottom 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.extensionManager.setting.get(this.monitorWidthId);
this.monitorUI?.updateMonitorSize(w, valueNumber);
},
};
}
});
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) => {
await this.updateServer({ switchCPU: value });
this.updateWidget(this.monitorCPUElement);
},
};
}
});
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) => {
await this.updateServer({ switchRAM: value });
this.updateWidget(this.monitorRAMElement);
},
};
}
});
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) => {
await this.updateServerGPU(index, { utilization: value });
this.updateWidget(monitorGPUNElement);
},
};
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) => {
await this.updateServerGPU(index, { vram: value });
this.updateWidget(monitorVRAMNElement);
},
};
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) => {
await this.updateServerGPU(index, { temperature: value });
this.updateWidget(monitorTemperatureNElement);
},
};
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) => {
await this.updateServer({ switchHDD: value });
this.updateWidget(this.monitorHDDElement);
},
};
this.settingsHDD = {
id: 'Crystools.WhichHdd',
name: 'Partition to show',
category: ['Crystools', this.menuPrefix + ' Show Hard Disk', 'Which'],
type: 'combo',
defaultValue: '/',
options: [],
onChange: async (value) => {
await this.updateServer({ whichHDD: value });
},
};
}
});
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.options = 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 }) => {
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.extensionManager.setting.get(this.monitorWidthId);
const h = app.extensionManager.setting.get(this.monitorHeightId);
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: (menuPosition) => {
let parentElement;
switch (menuPosition) {
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.extensionManager.setting.get(monitorSettings.id);
this.monitorUI.showMonitor(monitorSettings, value);
}
}
});
Object.defineProperty(this, "updateServer", {
enumerable: true,
configurable: true,
writable: true,
value: async (data) => {
const resp = await api.fetchApi('/crystools/monitor', {
method: 'PATCH',
body: JSON.stringify(data),
cache: 'no-store',
});
if (resp.status === 200) {
return await resp.text();
}
throw new Error(resp.statusText);
}
});
Object.defineProperty(this, "updateServerGPU", {
enumerable: true,
configurable: true,
writable: true,
value: async (index, data) => {
const resp = await api.fetchApi(`/crystools/monitor/GPU/${index}`, {
method: 'PATCH',
body: JSON.stringify(data),
cache: 'no-store',
});
if (resp.status === 200) {
return await resp.text();
}
throw new Error(resp.statusText);
}
});
Object.defineProperty(this, "getHDDsFromServer", {
enumerable: true,
configurable: true,
writable: true,
value: async () => {
return this.getDataFromServer('HDD');
}
});
Object.defineProperty(this, "getGPUsFromServer", {
enumerable: true,
configurable: true,
writable: true,
value: async () => {
return this.getDataFromServer('GPU');
}
});
Object.defineProperty(this, "getDataFromServer", {
enumerable: true,
configurable: true,
writable: true,
value: async (what) => {
const resp = await api.fetchApi(`/crystools/monitor/${what}`, {
method: 'GET',
cache: 'no-store',
});
if (resp.status === 200) {
return await resp.json();
}
throw new Error(resp.statusText);
}
});
Object.defineProperty(this, "setup", {
enumerable: true,
configurable: true,
writable: true,
value: () => {
if (this.monitorUI) {
return;
}
this.createSettingsRate();
this.createSettingsMonitorHeight();
this.createSettingsMonitorWidth();
this.createSettingsCPU();
this.createSettingsRAM();
this.createSettingsHDD();
this.createSettings();
const currentRate = parseFloat(app.extensionManager.setting.get(this.settingsRate.id));
this.menuDisplayOption = app.extensionManager.setting.get(ComfyKeyMenuDisplayOption);
app.ui.settings.addEventListener(`${ComfyKeyMenuDisplayOption}.change`, (e) => {
this.updateDisplay(e.detail.value);
});
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", {
enumerable: true,
configurable: true,
writable: true,
value: () => {
api.addEventListener('crystools.monitor', (event) => {
if (event?.detail === undefined) {
return;
}
this.monitorUI.updateDisplay(event.detail);
}, false);
}
});
}
}
const crystoolsMonitor = new CrystoolsMonitor();
app.registerExtension({
name: crystoolsMonitor.idExtensionName,
setup: crystoolsMonitor.setup,
});

View File

@@ -0,0 +1,588 @@
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';
// enum MonitorPosition {
// 'Top' = 'Top',
// 'Sidebar' = 'Sidebar',
// 'Floating' = 'Floating',
// }
class CrystoolsMonitor {
readonly idExtensionName = 'Crystools.monitor';
private readonly menuPrefix = commonPrefix;
private menuDisplayOption: MenuDisplayOptions = MenuDisplayOptions.Disabled;
private crystoolsButtonGroup: ComfyButtonGroup = null;
// private settingsMonitorPosition: 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[] = [];
private monitorUI: MonitorUI;
// private readonly monitorPositionId = 'Crystools.MonitorPosition';
private readonly monitorWidthId = 'Crystools.MonitorWidth';
private readonly monitorWidth = 60;
private readonly monitorHeightId = 'Crystools.MonitorHeight';
private readonly monitorHeight = 30;
// NO POSIBLE TO IMPLEMENT INSIDE THE PANEL
// createSettingsMonitorPosition = (): void => {
// const position = app.extensionManager.setting.get(this.monitorPositionId);
// console.log('position', position);
// this.settingsMonitorPosition = {
// id: this.monitorPositionId,
// name: 'Position (floating not implemented yet)',
// category: ['Crystools', this.menuPrefix + ' Configuration', 'position'],
// tooltip: 'Only for new UI',
// experimental: true,
// // data: [],
// type: 'combo',
// options: [
// MonitorPoistion.Top,
// MonitorPoistion.Sidebar,
// MonitorPoistion.Floating
// ],
//
// defaultValue: MonitorPoistion.Sidebar,
// // @ts-ignore
// onChange: (_value: string): void => {
// // if (this.monitorUI) {
// // console.log('onChange', _value);
// // this.moveMonitor(this.menuDisplayOption);
// // }
// },
// };
// };
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: 2,
step: .25,
},
defaultValue: .5,
// @ts-ignore
onChange: async(value: string): Promise<void> => {
let valueNumber: number;
try {
valueNumber = parseFloat(value);
if (isNaN(valueNumber)) {
throw new Error('invalid value');
}
} catch (error) {
console.error(error);
return;
}
try {
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.monitorUI.updateDisplay(data);
}
this.monitorUI?.updateAllAnimationDuration(valueNumber);
},
};
};
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 top/bottom 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.extensionManager.setting.get(this.monitorHeightId);
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 top/bottom 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.extensionManager.setting.get(this.monitorWidthId);
this.monitorUI?.updateMonitorSize(w, 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<void> => {
await this.updateServer({switchCPU: value});
this.updateWidget(this.monitorCPUElement);
},
};
};
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<void> => {
await this.updateServer({switchRAM: value});
this.updateWidget(this.monitorRAMElement);
},
};
};
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<void> => {
await this.updateServerGPU(index, {utilization: value});
this.updateWidget(monitorGPUNElement);
},
};
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<void> => {
await this.updateServerGPU(index, {vram: value});
this.updateWidget(monitorVRAMNElement);
},
};
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<void> => {
await this.updateServerGPU(index, {temperature: value});
this.updateWidget(monitorTemperatureNElement);
},
};
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<void> => {
await this.updateServer({switchHDD: value});
this.updateWidget(this.monitorHDDElement);
},
};
this.settingsHDD = {
id: 'Crystools.WhichHdd',
name: 'Partition to show',
category: ['Crystools', this.menuPrefix + ' Show Hard Disk', 'Which'],
type: 'combo',
defaultValue: '/',
options: [],
// @ts-ignore
onChange: async(value: string): Promise<void> => {
await this.updateServer({whichHDD: value});
},
};
};
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.settingsMonitorPosition);
app.ui.settings.addSetting(this.monitorRAMElement);
app.ui.settings.addSetting(this.monitorCPUElement);
void this.getHDDsFromServer().then((data: string[]): void => {
// @ts-ignore
this.settingsHDD.options = data;
app.ui.settings.addSetting(this.settingsHDD);
});
app.ui.settings.addSetting(this.monitorHDDElement);
void this.getGPUsFromServer().then((gpus: TGpuName[]): void => {
let moreThanOneGPU = false;
if (gpus.length > 1) {
moreThanOneGPU = true;
}
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.extensionManager.setting.get(this.monitorWidthId);
const h = app.extensionManager.setting.get(this.monitorHeightId);
this.monitorUI.updateMonitorSize(w, h);
};
updateDisplay = (value: MenuDisplayOptions): void => {
if (value !== this.menuDisplayOption) {
this.menuDisplayOption = value;
this.moveMonitor(this.menuDisplayOption);
}
};
moveMonitor = (menuPosition: MenuDisplayOptions): void => {
// console.log('moveMonitor', menuPosition);
// setTimeout(() => {
let parentElement: Element | null | undefined;
switch (menuPosition) {
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:
// const position = app.extensionManager.setting.get(this.monitorPositionId);
// if(position === MonitorPosition.Top) {
app.menu?.settingsGroup.element.before(this.crystoolsButtonGroup.element);
// } else {
// parentElement = document.getElementsByClassName('comfy-vue-side-bar-header')[0];
// if(parentElement){
// parentElement.insertBefore(this.crystoolsButtonGroup.element, parentElement.firstChild);
// } else {
// console.error('Crystools: parentElement to move monitors not found! back to top');
// app.ui.settings.setSettingValue(this.monitorPositionId, MonitorPoistion.Top);
// }
// }
}
// }, 100);
};
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.extensionManager.setting.get(monitorSettings.id);
this.monitorUI.showMonitor(monitorSettings, value);
}
};
updateServer = async(data: TStatsSettings): Promise<string> => {
const resp = await api.fetchApi('/crystools/monitor', {
method: 'PATCH',
body: JSON.stringify(data),
cache: 'no-store',
});
if (resp.status === 200) {
return await resp.text();
}
throw new Error(resp.statusText);
};
updateServerGPU = async(index: number, data: TGpuSettings): Promise<string> => {
const resp = await api.fetchApi(`/crystools/monitor/GPU/${index}`, {
method: 'PATCH',
body: JSON.stringify(data),
cache: 'no-store',
});
if (resp.status === 200) {
return await resp.text();
}
throw new Error(resp.statusText);
};
getHDDsFromServer = async(): Promise<string[]> => {
return this.getDataFromServer('HDD');
};
getGPUsFromServer = async(): Promise<TGpuName[]> => {
return this.getDataFromServer<TGpuName>('GPU');
};
getDataFromServer = async <T>(what: string): Promise<T[]> => {
const resp = await api.fetchApi(`/crystools/monitor/${what}`, {
method: 'GET',
cache: 'no-store',
});
if (resp.status === 200) {
return await resp.json();
}
throw new Error(resp.statusText);
};
setup = (): void => {
if (this.monitorUI) {
return;
}
// this.createSettingsMonitorPosition();
this.createSettingsRate();
this.createSettingsMonitorHeight();
this.createSettingsMonitorWidth();
this.createSettingsCPU();
this.createSettingsRAM();
this.createSettingsHDD();
this.createSettings();
const currentRate =
parseFloat(app.extensionManager.setting.get(this.settingsRate.id));
this.menuDisplayOption = app.extensionManager.setting.get(ComfyKeyMenuDisplayOption);
app.ui.settings.addEventListener(`${ComfyKeyMenuDisplayOption}.change`, (e: any) => {
this.updateDisplay(e.detail.value);
},
);
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();
};
registerListeners = (): void => {
api.addEventListener('crystools.monitor', (event: CustomEvent) => {
if (event?.detail === undefined) {
return;
}
this.monitorUI.updateDisplay(event.detail);
}, false);
};
}
const crystoolsMonitor = new CrystoolsMonitor();
app.registerExtension({
name: crystoolsMonitor.idExtensionName,
setup: crystoolsMonitor.setup,
});

View File

@@ -0,0 +1,26 @@
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;
maxVRAMUsed: Record<number, number>;
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;
resetMaxVRAM: () => void;
}

View File

@@ -0,0 +1,303 @@
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, "maxVRAMUsed", {
enumerable: true,
configurable: true,
writable: true,
value: {}
});
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) {
const gpuIndex = parseInt(monitorSettings.monitorTitle?.split(':')[0] || '0');
if (!this.maxVRAMUsed[gpuIndex] || this.maxVRAMUsed[gpuIndex] > total) {
this.maxVRAMUsed[gpuIndex] = 0;
}
if (used > this.maxVRAMUsed[gpuIndex]) {
this.maxVRAMUsed[gpuIndex] = used;
}
postfix = ` - ${formatBytes(used)} / ${formatBytes(total)}`;
postfix += ` Max: ${formatBytes(this.maxVRAMUsed[gpuIndex])}`;
}
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 = `#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';
}
}
});
Object.defineProperty(this, "resetMaxVRAM", {
enumerable: true,
configurable: true,
writable: true,
value: () => {
this.maxVRAMUsed = {};
}
});
this.createDOM();
this.styleSheet = createStyleSheet('crystools-monitors-size');
}
}

View File

@@ -0,0 +1,250 @@
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;
maxVRAMUsed: Record<number, number> = {}; // Add this to track max VRAM per GPU
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);
}
});
};
// eslint-disable-next-line complexity
updateMonitor = (monitorSettings: TMonitorSettings, percent: number, used?: number, total?: number): void => {
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 = '';
// Add max VRAM tracking for VRAM monitors
if (used !== undefined && total !== undefined) {
// Extract GPU index from monitorTitle (assuming format "X: GPU Name")
const gpuIndex = parseInt(monitorSettings.monitorTitle?.split(':')[0] || '0');
// Initialize max VRAM if not set or glitch
if (!this.maxVRAMUsed[gpuIndex] || this.maxVRAMUsed[gpuIndex]! > total) {
this.maxVRAMUsed[gpuIndex] = 0;
}
// Update max VRAM if current usage is higher
if ( used > this.maxVRAMUsed[gpuIndex]!) {
this.maxVRAMUsed[gpuIndex] = used;
}
postfix = ` - ${formatBytes(used)} / ${formatBytes(total)}`;
// Add max VRAM to tooltip
postfix += ` Max: ${formatBytes(this.maxVRAMUsed[gpuIndex]!)}`;
}
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 = `#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';
}
};
resetMaxVRAM = (): void => {
this.maxVRAMUsed = {};
};
}

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,187 @@
import { app, api } from './comfy/index.js';
import { commonPrefix } from './common.js';
import { ProgressBarUI } from './progressBarUI.js';
import { ComfyKeyMenuDisplayOption, EStatus, MenuDisplayOptions } from './progressBarUIBase.js';
class CrystoolsProgressBar {
constructor() {
Object.defineProperty(this, "idExtensionName", {
enumerable: true,
configurable: true,
writable: true,
value: 'Crystools.progressBar'
});
Object.defineProperty(this, "idShowProgressBar", {
enumerable: true,
configurable: true,
writable: true,
value: 'Crystools.ProgressBar'
});
Object.defineProperty(this, "defaultShowStatus", {
enumerable: true,
configurable: true,
writable: true,
value: true
});
Object.defineProperty(this, "menuPrefix", {
enumerable: true,
configurable: true,
writable: true,
value: commonPrefix
});
Object.defineProperty(this, "menuDisplayOption", {
enumerable: true,
configurable: true,
writable: true,
value: MenuDisplayOptions.Disabled
});
Object.defineProperty(this, "currentStatus", {
enumerable: true,
configurable: true,
writable: true,
value: EStatus.executed
});
Object.defineProperty(this, "currentProgress", {
enumerable: true,
configurable: true,
writable: true,
value: 0
});
Object.defineProperty(this, "currentNode", {
enumerable: true,
configurable: true,
writable: true,
value: undefined
});
Object.defineProperty(this, "timeStart", {
enumerable: true,
configurable: true,
writable: true,
value: 0
});
Object.defineProperty(this, "progressBarUI", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "createSettings", {
enumerable: true,
configurable: true,
writable: true,
value: () => {
app.ui.settings.addSetting({
id: this.idShowProgressBar,
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.progressBarUI.showProgressBar,
});
}
});
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) {
this.progressBarUI.updateDisplay(this.currentStatus, this.timeStart, this.currentProgress);
}
}
});
Object.defineProperty(this, "setup", {
enumerable: true,
configurable: true,
writable: true,
value: () => {
if (this.progressBarUI) {
this.progressBarUI
.showProgressBar(app.extensionManager.setting.get(this.idShowProgressBar));
return;
}
this.menuDisplayOption = app.extensionManager.setting.get(ComfyKeyMenuDisplayOption);
app.ui.settings.addEventListener(`${ComfyKeyMenuDisplayOption}.change`, (e) => {
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 {
console.error('Crystools: parentElement to move monitors not found!', parentElement);
}
this.createSettings();
this.updateDisplay(this.menuDisplayOption);
this.registerListeners();
}
});
Object.defineProperty(this, "registerListeners", {
enumerable: true,
configurable: true,
writable: true,
value: () => {
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.menuDisplayOption);
}, false);
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.menuDisplayOption);
}, false);
api.addEventListener('executed', ({ detail }) => {
if (detail?.node) {
this.currentNode = detail.node;
}
this.updateDisplay(this.menuDisplayOption);
}, false);
api.addEventListener('execution_start', ({ _detail }) => {
this.currentStatus = EStatus.executing;
this.timeStart = Date.now();
this.updateDisplay(this.menuDisplayOption);
}, false);
api.addEventListener('execution_error', ({ _detail }) => {
this.currentStatus = EStatus.execution_error;
this.updateDisplay(this.menuDisplayOption);
}, false);
}
});
Object.defineProperty(this, "centerNode", {
enumerable: true,
configurable: true,
writable: true,
value: () => {
const id = this.currentNode;
if (!id) {
return;
}
const node = app.graph.getNodeById(id);
if (!node) {
return;
}
app.canvas.centerOnNode(node);
}
});
}
}
const crystoolsProgressBar = new CrystoolsProgressBar();
app.registerExtension({
name: crystoolsProgressBar.idExtensionName,
setup: crystoolsProgressBar.setup,
});

View File

@@ -0,0 +1,140 @@
import { app, api } from './comfy/index.js';
import { commonPrefix } from './common.js';
import { ProgressBarUI } from './progressBarUI.js';
import { ComfyKeyMenuDisplayOption, EStatus, MenuDisplayOptions } from './progressBarUIBase.js';
class CrystoolsProgressBar {
idExtensionName = 'Crystools.progressBar';
idShowProgressBar = 'Crystools.ProgressBar';
defaultShowStatus = true;
menuPrefix = commonPrefix;
menuDisplayOption: MenuDisplayOptions = MenuDisplayOptions.Disabled;
currentStatus = EStatus.executed;
currentProgress = 0;
currentNode?: number = undefined;
timeStart = 0;
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: '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.progressBarUI.showProgressBar,
});
};
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) {
this.progressBarUI.updateDisplay(this.currentStatus, this.timeStart, this.currentProgress);
}
};
// automatically called by ComfyUI
setup = (): void => {
if (this.progressBarUI) {
this.progressBarUI
.showProgressBar(app.extensionManager.setting.get(this.idShowProgressBar));
return;
}
this.menuDisplayOption = app.extensionManager.setting.get(ComfyKeyMenuDisplayOption);
app.ui.settings.addEventListener(`${ComfyKeyMenuDisplayOption}.change`, (e: any) => {
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 {
console.error('Crystools: parentElement to move monitors not found!', parentElement);
}
this.createSettings();
this.updateDisplay(this.menuDisplayOption);
this.registerListeners();
};
registerListeners = (): void => {
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.menuDisplayOption);
}, false);
api.addEventListener('progress', ({detail}: any) => {
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.menuDisplayOption);
}, false);
api.addEventListener('executed', ({detail}: any) => {
if (detail?.node) {
this.currentNode = detail.node;
}
this.updateDisplay(this.menuDisplayOption);
}, false);
api.addEventListener('execution_start', ({_detail}: any) => {
this.currentStatus = EStatus.executing;
this.timeStart = Date.now();
this.updateDisplay(this.menuDisplayOption);
}, false);
api.addEventListener('execution_error', ({_detail}: any) => {
this.currentStatus = EStatus.execution_error;
this.updateDisplay(this.menuDisplayOption);
}, false);
};
centerNode = (): void => {
const id = this.currentNode;
if (!id) {
return;
}
const node = app.graph.getNodeById(id);
if (!node) {
return;
}
app.canvas.centerOnNode(node);
};
}
const crystoolsProgressBar = new CrystoolsProgressBar();
app.registerExtension({
name: crystoolsProgressBar.idExtensionName,
setup: crystoolsProgressBar.setup,
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 = {}));

View File

@@ -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',
}

View File

@@ -0,0 +1,61 @@
type TGpuStatData = {
gpu_utilization: number,
gpu_temperature: number,
vram_total: number,
vram_used: number,
vram_used_percent: number,
}
type TGpuSettings = {
utilization?: boolean,
vram?: boolean,
temperature?: boolean,
}
type TGpuName = {
name: string,
index: number,
}
type TStatsData = {
cpu_utilization: number,
device: string,
gpus: TGpuStatData[],
hdd_total: number,
hdd_used: number,
hdd_used_percent: number,
ram_total: number,
ram_used: number,
ram_used_percent: number,
}
type TStatsSettings = {
rate?: number,
switchCPU?: boolean,
switchGPU?: boolean,
switchHDD?: boolean,
switchRAM?: boolean,
switchVRAM?: boolean,
whichHDD?: string,
}
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' | 'slider' | 'combo',
defaultValue: boolean | number | string,
data?: any,
onChange: (value: boolean | number | string) => Promise<void>,
htmlMonitorRef?: HTMLDivElement,
htmlMonitorSliderRef?: HTMLDivElement,
htmlMonitorLabelRef?: HTMLDivElement,
cssColor: string,
cssColorFinal?: string,
}

View File

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

View File

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

View File

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