From da7bd64e96142e5f6bf0d5990c49870c583d84af Mon Sep 17 00:00:00 2001 From: AlekPet Date: Sun, 12 Apr 2026 19:45:31 +0300 Subject: [PATCH] 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 --- .gitignore | 4 +- IDENode/ide_node.py | 88 ++++- web_alekpet_nodes/ide_node.js | 364 +++++++++++++++--- .../lib/idenode/codes/javascript/add.json | 9 + .../lib/idenode/codes/python/add.json | 9 + 5 files changed, 424 insertions(+), 50 deletions(-) create mode 100644 web_alekpet_nodes/lib/idenode/codes/javascript/add.json create mode 100644 web_alekpet_nodes/lib/idenode/codes/python/add.json diff --git a/.gitignore b/.gitignore index 24641f8..cb6234f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ __pycache__ -settings_nodes/ \ No newline at end of file +settings_nodes/ +web_alekpet_nodes/lib/idenode/**/*.json +!web_alekpet_nodes/lib/idenode/**/add.json \ No newline at end of file diff --git a/IDENode/ide_node.py b/IDENode/ide_node.py index 009ae1e..f339378 100644 --- a/IDENode/ide_node.py +++ b/IDENode/ide_node.py @@ -1,3 +1,4 @@ +from pathlib import Path import types from server import PromptServer from aiohttp import web @@ -5,6 +6,12 @@ import time import json import re +# Directory node +DIR_IDENODE = Path(__file__) + +# Directory with codes +DIR_IDENODE_CODES = DIR_IDENODE.parents[1] / "web_alekpet_nodes" / "lib" / "idenode" / "codes" + remove_type_name = re.compile(r"(\{.*\})", re.I | re.M) # Hack: string type that is always equal in not equal comparisons, thanks pythongosssss @@ -17,7 +24,26 @@ PY_CODE = AnyType("*") IDEs_DICT = {} -@PromptServer.instance.routes.post("/alekpet/check_js_complete") +def isFileName(filename): + if ( + not filename + and filename is not None + and (type(filename) == str and filename.strip() == "") + ): + print("Filename is incorrect") + return False + return True + + +def getFilesInDir(folder, ext="*.json"): + if not folder.exists(): + raise ValueError(f"[IDENode] Not found directory '{folder}'!") + + list_files = [item.name for item in folder.rglob(ext) if not item.is_dir()] + return list_files + + +@PromptServer.instance.routes.post("/alekpet/ide_node_check_js_complete") async def check_js_complete(request): json_data = await request.json() unique_id = json_data.get("unique_id", None) @@ -36,6 +62,66 @@ async def check_js_complete(request): return web.json_response({"status": "Error"}) +@PromptServer.instance.routes.get("/alekpet/ide_node_load_codes/{lang}") +async def load_codes(request): + lang = request.match_info["lang"] + list_codes = getFilesInDir(DIR_IDENODE_CODES / lang) + + return web.json_response( + {"codes": list_codes, "language": lang} + ) + +@PromptServer.instance.routes.post("/alekpet/ide_node_save_code") +async def save_code(request): + try: + json_data = await request.json() + inputs = json_data.get("inputs", []) + outputs = json_data.get("outputs", []) + language = json_data.get("language", "python") + code = json_data.get("code", "") + filename = json_data.get("filename") + + if not isFileName(filename): + return web.json_response( + {"message": "Filename file code is incorrect!"}, status=400 + ) + + if not language.strip(): + return web.json_response( + {"message": "The programming language is incorrect!"}, status=400 + ) + + json_data = { + "language": language, + "inputs": inputs, + "outputs": outputs, + "code": code, + } + + save_path = DIR_IDENODE_CODES / language / filename + + if not save_path.suffix.strip(): + save_path = save_path.with_suffix('.json') + + with open(save_path, "w+", encoding="utf-8", errors="replace") as file: + json.dump(json_data, file, ensure_ascii=False, indent=2) + + list_codes = getFilesInDir(DIR_IDENODE_CODES / language) + return web.json_response( + { + "codes": list_codes, + "language": language, + "message": f"Code from '{language}' save to '{save_path.name}' successfully!", + }, + status=200, + ) + + except OSError as e: + return web.json_response( + {"error": "Error: %s - %s." % (e.filename, e.strerror)}, status=500 + ) + + def wait_js_complete(unique_id, time_out=40, poll_interval=0.2): start_time = time.time() while time.time() - start_time < time_out: diff --git a/web_alekpet_nodes/ide_node.js b/web_alekpet_nodes/ide_node.js index c3e0985..b784286 100644 --- a/web_alekpet_nodes/ide_node.js +++ b/web_alekpet_nodes/ide_node.js @@ -91,17 +91,103 @@ 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: (node, inputName, inputData, app) => { + PYCODE: async (node, inputName, inputData, app) => { // Wrapper and codeElement const codeElementWrapper = makeElement("div", { style: { height: "100%", marginTop: "-15px" }, @@ -162,50 +248,14 @@ result = str(my(23, 9))`, } 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; }; } - // 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; - } - // Add input vars node.addWidget( "button", @@ -318,11 +368,214 @@ result = str(my(23, 9))`, } ); + // 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 - node.addWidget("button", "Clear", "clear_code", () => { - widget.editor.setValue(""); - saveValue(); - }); + 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) { @@ -380,6 +633,12 @@ result = str(my(23, 9))`, // }); // } + // 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 }) => { @@ -443,7 +702,7 @@ result = str(my(23, 9))`, res(result_run_code); }).then((result_code) => { api - .fetchApi("/alekpet/check_js_complete", { + .fetchApi("/alekpet/ide_node_check_js_complete", { method: "POST", body: JSON.stringify({ unique_id: this.id.toString(), @@ -508,7 +767,6 @@ result = str(my(23, 9))`, ctx.fillStyle = !colorType || colorType === "" ? "#AAA" : colorType; ctx.font = "12px Arial, sans-serif"; ctx.textAlign = "left"; - ctx.textBaseline = "middle"; ctx.fillText( `[${type === "*" ? "any" : type.toLowerCase()}]`, nameSize.width + 25, @@ -544,6 +802,16 @@ result = str(my(23, 9))`, "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) { diff --git a/web_alekpet_nodes/lib/idenode/codes/javascript/add.json b/web_alekpet_nodes/lib/idenode/codes/javascript/add.json new file mode 100644 index 0000000..2084325 --- /dev/null +++ b/web_alekpet_nodes/lib/idenode/codes/javascript/add.json @@ -0,0 +1,9 @@ +{ + "language": "javascript", + "inputs": [ + { "name": "a", "type": "*" }, + { "name": "b", "type": "*" } + ], + "outputs": [{ "name": "result", "type": "*" }], + "code": "result = a + b" +} diff --git a/web_alekpet_nodes/lib/idenode/codes/python/add.json b/web_alekpet_nodes/lib/idenode/codes/python/add.json new file mode 100644 index 0000000..2b8bdcd --- /dev/null +++ b/web_alekpet_nodes/lib/idenode/codes/python/add.json @@ -0,0 +1,9 @@ +{ + "language": "python", + "inputs": [ + { "name": "a", "type": "*" }, + { "name": "b", "type": "*" } + ], + "outputs": [{ "name": "result", "type": "*" }], + "code": "result = a + b" +}