import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
import "./lib/idenode/ace-builds/src-min-noconflict/ace.js";
import { addStylesheet } from "../../scripts/utils.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 --";
// RegExp
const symbolsIncorrectFileName = /[/\\?%*:|"<>]/g;
// Save data to workflow forced!
function saveValue() {
app?.extensionManager?.workflow?.activeWorkflow?.changeTracker?.captureCanvasState();
}
// 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",
init(app) {
addStylesheet("css/idenode/ide_node_styles.css", import.meta.url);
},
getCustomWidgets(app) {
return {
PYCODE: async (node, inputName, inputData, app) => {
// Wrapper and codeElement
const codeElementWrapper = makeElement("div", {
style: { height: "100%", margin: "-10px 0 10px 0" },
});
const codeElement = makeElement("pre", {
innerHTML:
inputData[1]?.default ||
`def my(a, b=1):
return a * b
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;
node.setDirtyCanvas(true, true);
};
}
// Load code
const codeLoad = node.addWidget(
"COMBO",
"Load code",
defaultComboValue,
async (codefile) => {
if (codefile === defaultComboValue) return;
// Confirm load code?
app.extensionManager.dialog
.confirm({
title: "Confirm load code",
message: "Are you sure you want load code?",
type: "default",
})
.then(async (result) => {
if (!result) {
// codeLoad.value = defaultComboValue;
return;
}
// 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 input
const inputsSet = new Set([
"language",
"theme_highlight",
"pycode",
...inputs.map((i) => i.name),
]);
for (let i = node.inputs.length - 1; i >= 0; i--) {
const input = node.inputs[i];
if (!inputsSet.has(input.name)) {
node.removeInput(i);
}
}
// Remove output
const outputSet = new Set([
"result",
...outputs.map((o) => o.name),
]);
for (let i = node.outputs.length - 1; i >= 0; i--) {
const output = node.outputs[i];
if (!outputSet.has(output.name)) {
node.removeOutput(i);
}
}
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,
}
);
// Remove node
node.onRemoved = function () {
for (const w of node?.widgets) {
if (w?.codeElement) w.codeElement.remove();
}
};
// Add input vars
const buttonAddInput = makeElement("button", {
class: [
"ide_node_controls_buttons",
"button_ide_node_addinput",
"ide_node_controls_first_row",
],
textContent: "โ INPUT VARIABLE",
title: "Add input variable",
onclick: 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,
`