Files
AlekPet-ComfyUI_Custom_Node…/web_alekpet_nodes/ide_node.js
T
AlekPet da7bd64e96 implemented new feautes
+ load code with inputs and outputs
+ added a code save button, allowing you to save written code with inputs and outputs
+ example codes (js and python) add.json in the directory ./web_alekpet_nodes/lib/idenode/codes/*
+ visual changes and other improvements
2026-04-12 19:45:31 +03:00

884 lines
27 KiB
JavaScript

import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
import "./lib/idenode/ace-builds/src-min-noconflict/ace.js";
import {
createWindowModal,
makeElement,
findWidget,
THEMES_MODAL_WINDOW,
comfyuiDesktopConfirm,
comfyuiDesktopPrompt,
} from "./utils.js";
ace.config.set(
"basePath",
new URL(
"./lib/idenode/ace-builds/src-min-noconflict/",
import.meta.url
).toString()
);
// Constants
const MAX_CHAR_VARNAME = 50;
const LIST_THEMES = [
"ambiance",
"chaos",
"chrome",
"cloud9_day",
"cloud9_night",
"cloud9_night_low_color",
"clouds",
"clouds_midnight",
"cloud_editor",
"cloud_editor_dark",
"cobalt",
"crimson_editor",
"dawn",
"dracula",
"dreamweaver",
"eclipse",
"github",
"github_dark",
"github_light_default",
"gob",
"gruvbox",
"gruvbox_dark_hard",
"gruvbox_light_hard",
"idle_fingers",
"iplastic",
"katzenmilch",
"kr_theme",
"kuroir",
"merbivore",
"merbivore_soft",
"monokai",
"mono_industrial",
"nord_dark",
"one_dark",
"pastel_on_dark",
"solarized_dark",
"solarized_light",
"sqlserver",
"terminal",
"textmate",
"tomorrow",
"tomorrow_night",
"tomorrow_night_blue",
"tomorrow_night_bright",
"tomorrow_night_eighties",
"twilight",
"vibrant_ink",
"xcode",
];
const DEFAULT_TEMPLATES = {
js: `// !!! Attention, do not insert unverified code !!!
// ---- Example code ----
// Globals inputs variables: var1, var2, var3, user variables ...
const runCode = () => {
const date = new Date().toJSON().replace("T"," ");
return date.slice(0,date.indexOf("."))
}
result = \`Hello ComfyUI with us today \${runCode()}!\``,
py: `# !!! Attention, do not insert unverified code !!!
# ---- Example code ----
# Globals inputs variables: var1, var2, var3, user variables ...
from time import strftime
def runCode():
nowDataTime = strftime("%Y-%m-%d %H:%M:%S")
return f"Hello ComfyUI with us today {nowDataTime}!"
result = runCode()
`,
};
// Default value select load code
const defaultComboValue = "-- Select code file --";
// Save data to workflow forced!
function saveValue() {
app?.extensionManager?.workflow?.activeWorkflow?.changeTracker?.checkState();
}
// Paint widget function
function paintWidget(paramsPaint) {
const oldDraw = this.drawWidget;
const that = this;
this.draw = function () {
// Save old params and set new
const old_params = {};
for (const param in paramsPaint) {
if (!LiteGraph.hasOwnProperty(param)) continue;
old_params[param] = LiteGraph[param];
LiteGraph[param] = paramsPaint[param];
}
oldDraw?.apply(that, arguments);
// Restore old params
for (const param in old_params) {
LiteGraph[param] = old_params[param];
}
};
}
// Load list codes
async function loadListCodes(language) {
try {
const response = await api.fetchApi(
`/alekpet/ide_node_load_codes/${language}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
);
return await response.json();
} catch (e) {
console.error(e);
return { codes: [], language };
}
}
// Checks Vars
function makeValidVariable(
varName,
iotype,
textContent,
regex = /^[a-z_][a-z0-9_]*$/i
) {
if (
!varName ||
varName.trim() === "" ||
varName.length > MAX_CHAR_VARNAME ||
!regex.test(varName) ||
iotype.some((i) => i.name === varName)
) {
createWindowModal({
textTitle: "WARNING",
textBody: [
makeElement("div", {
style: { fontSize: "0.7rem" },
innerHTML: textContent,
}),
],
...THEMES_MODAL_WINDOW.warning,
options: {
auto: {
autohide: true,
autoremove: true,
autoshow: true,
timewait: 1000,
},
close: { showClose: false },
overlay: { overlay_enabled: true },
parent: widget.codeElement,
},
});
return false;
}
return true;
}
// Register extensions
app.registerExtension({
name: "alekpet.IDENode",
getCustomWidgets(app) {
return {
PYCODE: async (node, inputName, inputData, app) => {
// Wrapper and codeElement
const codeElementWrapper = makeElement("div", {
style: { height: "100%", marginTop: "-15px" },
});
const codeElement = makeElement("pre", {
innerHTML:
inputData[1]?.default ||
`def my(a, b=1):
return a * b<br>
result = str(my(23, 9))`,
style: { height: "100%" },
});
codeElementWrapper.appendChild(codeElement);
// Editor
const editor = ace.edit(codeElement);
editor.setTheme("ace/theme/monokai");
editor.session.setMode("ace/mode/python");
// Theme list
const themeList = node.addWidget(
"combo",
"theme_highlight",
"monokai",
(v) => {
widget.editor.setTheme(`ace/theme/${themeList.value}`);
},
{
values: LIST_THEMES,
serialize: false,
}
);
// Languages list
const widgetLang_id = findWidget(node, "language", "name", "findIndex");
if (widgetLang_id !== -1) {
node.widgets[widgetLang_id].callback = async (v) => {
widget.editor.setTheme(`ace/theme/${themeList.value}`);
if (await comfyuiDesktopConfirm("Clear code?"))
widget.editor.setValue("");
if (await comfyuiDesktopConfirm("Paste template?")) {
let defaultCode = null;
if (v === "javascript") {
defaultCode = DEFAULT_TEMPLATES.js;
} else if (v === "python") {
defaultCode = DEFAULT_TEMPLATES.py;
}
if (defaultCode) {
widget.editor.setValue(defaultCode);
widget.editor.clearSelection();
}
}
widget.editor.session.setMode(`ace/mode/${v}`);
// Load list codes by language
const { codes } = await loadListCodes(v);
codeLoad.options.values = [defaultComboValue].concat(...codes);
codeLoad.value = defaultComboValue;
};
}
// Add input vars
node.addWidget(
"button",
"Add Input variable",
"add_input_variable",
async () => {
// Input name variable and check
const varsCount = node?.inputs.filter((i) =>
/^var[0-9]+$/.test(i?.name)
);
const nameInput = varsCount?.length
? `var${varsCount.length + 1}`
: "var1";
let varName = await comfyuiDesktopPrompt(
"Variable",
"Enter input variable name:",
nameInput
);
varName = varName.trim();
if (
!makeValidVariable(
varName,
node?.inputs,
`<h3 style="margin: 0;">Variable for <span style="color: limegreen">Input</span> name is incorrect!</h3><ul style="text-align:left;padding: 2px;margin-left: 5%;"><li>variable <span style="color:orange;font-weight: 600;">${varName}</span> already present in the inputs</li><li>starts with a number</li><li>has spaces or tabs</li><li>is empty</li><li>variable name is greater ${MAX_CHAR_VARNAME}</li></ul>`
)
)
return;
// Type variable and check
let type = await comfyuiDesktopPrompt(
"Type",
"Enter type data output (default: *):",
"*"
);
type = type.trim();
if (
!makeValidVariable(
type,
node?.inputs,
`<h3 style="margin: 0;">Type value is incorrect!</h3><ul style="text-align:left;padding: 2px;"><li>has spaces or tabs</li><li>is empty</li><li>type value length is greater ${MAX_CHAR_VARNAME}</li></ul>`,
/^[*a-z_][a-z0-9_]*$/i
)
)
return;
const currentWidth = node.size[0];
node.addInput(varName, type);
node.setSize([currentWidth, node.size[1]]);
saveValue();
}
);
// Add output vars
node.addWidget(
"button",
"Add Output variable",
"add_output_variable",
async () => {
const currentWidth = node.size[0];
// Output name variable
const resultCount = node?.outputs.filter((i) =>
/^result[0-9]+$/.test(i?.name)
);
const nameOutput = resultCount?.length
? `result${resultCount.length + 1}`
: "result1";
let varName = await comfyuiDesktopPrompt(
"Variable",
"Enter output variable name:",
nameOutput
);
// Check output variable name
varName = varName.trim();
if (
!makeValidVariable(
varName,
node?.outputs,
`<h3 style="margin: 0;">Variable for <span style="color: pink">Output</span> name is incorrect!</h3><ul style="text-align:left;padding: 2px;margin-left: 5%;"><li>variable <span style="color:orange;font-weight: 600;">${varName}</span> already present in the outputs</li><li>starts with a number</li><li>has spaces or tabs</li><li>is empty</li><li>variable name is greater ${MAX_CHAR_VARNAME}</li></ul>`
)
)
return;
// Type variable and check
let type = await comfyuiDesktopPrompt(
"Type",
"Enter type data output (default: *):",
"*"
);
type = type.trim();
if (
!makeValidVariable(
type,
node?.outputs,
`<h3 style="margin: 0;">Type value is incorrect!</h3><ul style="text-align:left;padding: 2px;"><li>has spaces or tabs</li><li>is empty</li><li>type value length is greater ${MAX_CHAR_VARNAME}</li></ul>`,
/^[*a-z_][a-z0-9_]*$/i
)
)
return;
node.addOutput(varName, type);
node.setSize([currentWidth, node.size[1]]);
saveValue();
}
);
// Save code
const save_code = node.addWidget(
"button",
"Save code",
"save_code",
async () => {
comfyuiDesktopPrompt(
"Code filename",
"Enter filename your code",
codeLoad.value === defaultComboValue ? "" : codeLoad.value
).then((filename) => {
if (!filename || !filename.trim()) return;
try {
api
.fetchApi("/alekpet/ide_node_save_code", {
method: "POST",
body: JSON.stringify({
filename: filename.trim(),
inputs: node.inputs
.filter((i) => !["language", "pycode"].includes(i.name))
.map((i) => ({
name: i.name,
type: i.type,
})),
outputs: node.outputs.map((o) => ({
name: o.name,
type: o.type,
})),
language: node.widgets[widgetLang_id].value,
code: widget.editor.getValue(),
}),
})
.then((res) => res.json())
.then((data) => {
const { codes, message, error } = data;
if (error) {
throw new Error(error);
}
app.extensionManager.toast.add({
severity: "success",
summary: "Success",
detail: message,
life: 3000,
});
codeLoad.options.values = [defaultComboValue].concat(codes);
});
} catch (e) {
app.extensionManager.toast.add({
severity: "error",
summary: "Error",
detail: e,
life: 3000,
});
console.error(e);
}
});
}
);
paintWidget.apply(save_code, [
{ WIDGET_TEXT_COLOR: "#30ce00", WIDGET_OUTLINE_COLOR: "#30ce00" },
]); // WIDGET_BGCOLOR: "#27a700"
// Load code
const codeLoad = node.addWidget(
"COMBO",
"Load code",
defaultComboValue,
async (codefile) => {
if (codefile === defaultComboValue) return;
// Confirm load code?
await app.extensionManager.dialog
.confirm({
title: "Confirm load code",
message: "Are you sure you want load code?",
type: "default",
})
.then((result) => {
if (!result) codeLoad.value = defaultComboValue;
});
// Load code
fetch(
new URL(
`./lib/idenode/codes/${node.widgets[widgetLang_id].value}/${codefile}`,
import.meta.url
).toString()
)
.then((res) => res.json())
.then((res) => {
const { inputs, outputs, code, language } = res;
let lang = language ?? "python";
// Set code language
node.widgets[widgetLang_id].value = lang;
widget.editor.session.setMode(`ace/mode/${lang}`);
const currentWidth = node.size[0];
// Remove all input
for (const input_idx in node.inputs) {
const input = node.inputs[input_idx];
if (
[
"language",
"theme_highlight",
...inputs.map((i) => i.name),
].includes(input.name)
)
continue;
if (input.link) {
app.graph.removeLink(input.link);
}
node.removeInput(input_idx);
}
// Remove all output
for (const output_idx in node.outputs) {
const output = node.outputs[output_idx];
if (
["result", ...outputs.map((o) => o.name)].includes(
output.name
)
)
continue;
if (output.link) {
app.graph.removeLink(output.link);
}
node.removeOutput(output_idx);
}
inputs.forEach((input) => {
const isNotInArray = !node.inputs.some(
(i) => i.name === input.name.trim()
);
if (isNotInArray)
node.addInput(input.name.trim(), input.type);
});
outputs.forEach((output) => {
const isNotInArray = !node.outputs.some(
(o) => o.name === output.name.trim()
);
if (isNotInArray)
node.addOutput(output.name.trim(), output.type);
});
// Set code
widget.editor.setValue(code);
widget.editor.clearSelection();
node.setSize([currentWidth, node.size[1]]);
node.setDirtyCanvas(true, true);
saveValue();
})
.catch((e) => {
createWindowModal({
textTitle: "ERROR",
textBody: [
makeElement("div", {
style: { fontSize: "0.7rem" },
innerHTML: e,
}),
],
...THEMES_MODAL_WINDOW.error,
options: {
auto: {
autohide: true,
autoremove: true,
autoshow: true,
timewait: 1500,
},
close: { showClose: false },
overlay: { overlay_enabled: true },
parent: widget.codeElement,
},
});
console.error(e);
});
},
{
values: [defaultComboValue],
default: defaultComboValue,
}
);
// Clear code button
const clearButton = node.addWidget(
"button",
"Clear",
"clear_code",
() => {
widget.editor.setValue("");
saveValue();
}
);
paintWidget.apply(clearButton, [
{ WIDGET_TEXT_COLOR: "#ee0101", WIDGET_OUTLINE_COLOR: "#ee0101" },
]);
node.onRemoved = function () {
for (const w of node?.widgets) {
if (w?.codeElement) w.codeElement.remove();
}
};
// Add DOMWidget
const widget = node.addDOMWidget(
"pycode",
"widget_pycode",
codeElementWrapper,
{
getValue() {
return widget.editor.getValue();
},
}
);
widget.editor = editor;
widget.codeElement = codeElement;
widget.editor.getSession().on("change", function (e) {
widget.value = widget.editor.getValue();
saveValue();
});
return widget;
},
};
},
async beforeRegisterNodeDef(nodeType, nodeData, app) {
// --- IDENode
if (nodeData.name === "IDENode") {
// Node Created
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = async function () {
const ret = onNodeCreated
? onNodeCreated.apply(this, arguments)
: undefined;
const node_title = await this.getTitle();
const nodeName = `${nodeData.name}_${this.id}`;
console.log(`Create ${nodeData.name}: ${nodeName}`);
this.name = nodeName;
// Create default inputs, when first create node
// if (this?.inputs?.length === 2) {
// ["var1", "var2", "var3"].forEach((inputName) => {
// const currentWidth = this.size[0];
// this.addInput(inputName, "*");
// this.setSize([currentWidth, this.size[1]]);
// });
// }
// Load list codes by language
const widgetLanguage = findWidget(this, "language", "name");
const widgetLoadCode = findWidget(this, "Load code", "name");
const { codes } = await loadListCodes(widgetLanguage.value);
widgetLoadCode.options.values = [defaultComboValue].concat(codes);
const widgetEditor = findWidget(this, "widget_pycode", "type");
// JS run
api.addEventListener("alekpet_js_result", async ({ detail }) => {
const { vars, unique_id } = detail;
if (
(vars && !Object.keys(vars).length) ||
+unique_id !== this.id ||
this._jsProcessing
) {
return;
}
this._jsProcessing = true;
await new Promise((res) => {
const edit_vars = JSON.parse(vars);
let code_run = "";
for (let [k, v] of Object.entries(edit_vars)) {
// Check type
if (v instanceof String || typeof v === "string") {
v = `"${v}"`;
} else if (v instanceof Object || typeof v === "object") {
v = JSON.stringify(v);
}
code_run += `let ${k} = ${v};\n`;
}
code_run += `${widgetEditor.editor.getValue()}\nreturn [${Object.keys(
edit_vars
)},]\n`;
let result_run_code = null;
try {
result_run_code = eval(`(function(){${code_run}}())`);
} catch (e) {
result_run_code = `Error in javascript code: ${e}`;
console.error(`<${this.name}> ${result_run_code}`);
createWindowModal({
textTitle: "Error in javascript code",
textBody: [
makeElement("div", {
style: { fontSize: "0.7rem" },
textContent: e,
}),
],
...THEMES_MODAL_WINDOW.error,
options: {
auto: {
autohide: true,
autoremove: true,
autoshow: true,
timewait: 1000,
},
close: { showClose: false },
overlay: { overlay_enabled: true },
parent: widgetEditor.codeElement,
},
});
res(Object.keys(edit_vars).map((v) => e.message));
}
res(result_run_code);
}).then((result_code) => {
api
.fetchApi("/alekpet/ide_node_check_js_complete", {
method: "POST",
body: JSON.stringify({
unique_id: this.id.toString(),
result_code,
}),
})
.then((res) => res.json())
.then((res) => {
res?.status === "Ok"
? console.log(
`%cJS complete ok: ${this.name}: ${res.status}`,
"color: green; font-weight: 600;"
)
: console.error(`Error JS complete: ${res.status}`);
this._jsProcessing = false;
})
.catch((err) => {
this._jsProcessing = false;
console.error(`Error JS complete: ${err}`);
});
});
});
this.setSize([500, 500]);
return ret;
};
const onDrawForeground = nodeType.prototype.onDrawForeground;
nodeType.prototype.onDrawForeground = function (ctx) {
const r = onDrawForeground?.apply?.(this, arguments);
if (this.flags?.collapsed) return r;
if (this?.outputs?.length) {
for (let o = 0; o < this.outputs.length; o++) {
const { name, type } = this.outputs[o];
const colorType = LGraphCanvas.link_type_colors[type.toUpperCase()];
const nameSize = ctx.measureText(name);
const typeSize = ctx.measureText(
`[${type === "*" ? "any" : type.toLowerCase()}]`
);
ctx.fillStyle = colorType === "" ? "#AAA" : colorType;
ctx.font = "12px Arial, sans-serif";
ctx.textAlign = "right";
ctx.fillText(
`[${type === "*" ? "any" : type.toLowerCase()}]`,
this.size[0] - nameSize.width - typeSize.width,
o * 20 + 19
);
}
}
if (this?.inputs?.length) {
const not_showing = ["language", "pycode"];
for (let i = 0; i < this.inputs.length; i++) {
const { name, type } = this.inputs[i];
if (not_showing.includes(name)) continue;
const colorType = LGraphCanvas.link_type_colors[type.toUpperCase()];
const nameSize = ctx.measureText(name);
ctx.fillStyle = !colorType || colorType === "" ? "#AAA" : colorType;
ctx.font = "12px Arial, sans-serif";
ctx.textAlign = "left";
ctx.fillText(
`[${type === "*" ? "any" : type.toLowerCase()}]`,
nameSize.width + 25,
i * 20 + 19 - not_showing.length * 20
);
}
}
return r;
};
// Node Configure
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function (node) {
onConfigure?.apply(this, arguments);
if (node?.widgets_values?.length) {
const widget_code_id = findWidget(
this,
"widget_pycode",
"type",
"findIndex"
);
const widget_theme_id = findWidget(
this,
"theme_highlight",
"name",
"findIndex"
);
const widget_language_id = findWidget(
this,
"language",
"name",
"findIndex"
);
const widget_load_code_id = findWidget(
this,
"Load code",
"name",
"findIndex"
);
this.widgets[widget_load_code_id].value =
this.widgets[widget_load_code_id].options?.default;
const editor = this.widgets[widget_code_id]?.editor;
if (editor) {
editor.setTheme(
`ace/theme/${this.widgets_values[widget_theme_id]}`
);
editor.session.setMode(
`ace/mode/${this.widgets_values[widget_language_id]}`
);
editor.setValue(this.widgets_values[widget_code_id]);
editor.clearSelection();
}
}
};
// ExtraMenuOptions
const getExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
nodeType.prototype.getExtraMenuOptions = function (_, options) {
getExtraMenuOptions?.apply(this, arguments);
const past_index = options.length - 1;
const past = options[past_index];
if (!!past) {
// Inputs remove
for (const input_idx in this.inputs) {
const input = this.inputs[input_idx];
if (["language", "theme_highlight"].includes(input.name)) continue;
options.splice(past_index + 1, 0, {
content: `Remove Input ${input.name}`,
callback: (e) => {
const currentWidth = this.size[0];
if (input.link) {
app.graph.removeLink(input.link);
}
this.removeInput(input_idx);
this.setSize([currentWidth, this.size[1]]);
saveValue();
},
});
}
// Output remove
for (const output_idx in this.outputs) {
const output = this.outputs[output_idx];
if (output.name === "result") continue;
options.splice(past_index + 1, 0, {
content: `Remove Output ${output.name}`,
callback: (e) => {
const currentWidth = this.size[0];
if (output.link) {
app.graph.removeLink(output.link);
}
this.removeOutput(output_idx);
this.setSize([currentWidth, this.size[1]]);
saveValue();
},
});
}
}
};
// end - ExtraMenuOptions
}
},
});