import { app } from "../../scripts/app.js"; import { api } from "../../scripts/api.js" import { sleep, show_message } from "./common.js"; import { GroupNodeConfig, GroupNodeHandler } from "../../extensions/core/groupNode.js"; import { ComfyDialog, $el } from "../../scripts/ui.js"; let pack_map = {}; let rpack_map = {}; export function isInstalled(name) { return rpack_map[name] != undefined; } export function getPureName(node) { // group nodes/ let category = null; if(node.category) { category = node.category.substring(12); } else { category = node.constructor.category?.substring(12); } if(category) { let purename = node.comfyClass.substring(category.length+1); return purename; } else if(node.comfyClass.startsWith('workflow/')) { return node.comfyClass.substring(9); } else { return node.comfyClass; } } function isValidVersionString(version) { const versionPattern = /^(\d+)\.(\d+)(\.(\d+))?$/; const match = version.match(versionPattern); return match !== null && parseInt(match[1], 10) >= 0 && parseInt(match[2], 10) >= 0 && (!match[3] || parseInt(match[4], 10) >= 0); } function register_pack_map(name, data) { if(data.packname) { pack_map[data.packname] = name; rpack_map[name] = data; } else { rpack_map[name] = data; } } function storeGroupNode(name, data, register=true) { let extra = app.graph.extra; if (!extra) app.graph.extra = extra = {}; let groupNodes = extra.groupNodes; if (!groupNodes) extra.groupNodes = groupNodes = {}; groupNodes[name] = data; if(register) { register_pack_map(name, data); } } export async function load_components() { let data = await api.fetchApi('/manager/component/loads', {method: "POST"}); let components = await data.json(); let start_time = Date.now(); let failed = []; let failed2 = []; for(let name in components) { if(app.graph.extra?.groupNodes?.[name]) { if(data) { let data = components[name]; let category = data.packname; if(data.category) { category += "/" + data.category; } if(category == '') { category = 'components'; } const config = new GroupNodeConfig(name, data); await config.registerType(category); register_pack_map(name, data); continue; } } let nodeData = components[name]; storeGroupNode(name, nodeData); const config = new GroupNodeConfig(name, nodeData); while(true) { try { let category = nodeData.packname; if(nodeData.category) { category += "/" + nodeData.category; } if(category == '') { category = 'components'; } await config.registerType(category); register_pack_map(name, nodeData); break; } catch { let elapsed_time = Date.now() - start_time; if (elapsed_time > 5000) { failed.push(name); break; } else { await sleep(100); } } } } // fallback1 for(let i in failed) { let name = failed[i]; if(app.graph.extra?.groupNodes?.[name]) { continue; } let nodeData = components[name]; storeGroupNode(name, nodeData); const config = new GroupNodeConfig(name, nodeData); while(true) { try { let category = nodeData.packname; if(nodeData.workflow.category) { category += "/" + nodeData.category; } if(category == '') { category = 'components'; } await config.registerType(category); register_pack_map(name, nodeData); break; } catch { let elapsed_time = Date.now() - start_time; if (elapsed_time > 10000) { failed2.push(name); break; } else { await sleep(100); } } } } // fallback2 for(let name in failed2) { let name = failed2[i]; let nodeData = components[name]; storeGroupNode(name, nodeData); const config = new GroupNodeConfig(name, nodeData); while(true) { try { let category = nodeData.workflow.packname; if(nodeData.workflow.category) { category += "/" + nodeData.category; } if(category == '') { category = 'components'; } await config.registerType(category); register_pack_map(name, nodeData); break; } catch { let elapsed_time = Date.now() - start_time; if (elapsed_time > 30000) { failed.push(name); break; } else { await sleep(100); } } } } } export async function uninstall_component(nodename, packname) { let body = { "name": nodename, "packname": packname }; const res = await api.fetchApi('/manager/component/remove', { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(body), }); if(res.status == 200) { delete rpack_map[nodename]; } else { console.log(`Failed to remove component: ${nodename}`); } } export async function save_as_component(node, version, author, prefix, nodename, packname, category) { let component_name = nodename; if(prefix) { component_name = `${prefix}::${nodename}`; } let subgraph = app.graph.extra?.groupNodes?.[component_name]; if(!subgraph) { subgraph = app.graph.extra?.groupNodes?.[getPureName(node)]; } subgraph.version = version; subgraph.author = author; subgraph.datetime = Date.now(); subgraph.packname = packname; subgraph.category = category; let body = { name: component_name, workflow: subgraph }; pack_map[packname] = component_name; rpack_map[component_name] = subgraph; const res = await api.fetchApi('/manager/component/save', { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(body), }); if(res.status == 200) { storeGroupNode(component_name, subgraph); const config = new GroupNodeConfig(component_name, subgraph); let category = body.workflow.packname; if(body.workflow.category) { category += "/" + body.workflow.category; } if(category == '') { category = 'components'; } await config.registerType(category); let path = await res.text(); show_message(`Component '${component_name}' is saved into:\n${path}`); } else show_message(`Failed to save component.`); } async function import_component(component_name, component, mode) { if(mode) { let body = { name: component_name, workflow: component }; const res = await api.fetchApi('/manager/component/save', { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(body) }); } let category = component.packname; if(component.category) { category += "/" + component.category; } if(category == '') { category = 'components'; } storeGroupNode(component_name, component); const config = new GroupNodeConfig(component_name, component); await config.registerType(category); } function restore_to_loaded_component(component_name) { if(rpack_map[component_name]) { let component = rpack_map[component_name]; storeGroupNode(component_name, component, false); const config = new GroupNodeConfig(component_name, component); config.registerType(component.category); } } // Using a timestamp prevents duplicate pastes and ensures the prevention of re-deletion of litegrapheditor_clipboard. let last_paste_timestamp = null; function versionCompare(v1, v2) { let ver1; let ver2; if(v1 && v1 != '') { ver1 = v1.split('.'); ver1[0] = parseInt(ver1[0]); ver1[1] = parseInt(ver1[1]); if(ver1.length == 2) ver1.push(0); else ver1[2] = parseInt(ver2[2]); } else { ver1 = [0,0,0]; } if(v2 && v2 != '') { ver2 = v2.split('.'); ver2[0] = parseInt(ver2[0]); ver2[1] = parseInt(ver2[1]); if(ver2.length == 2) ver2.push(0); else ver2[2] = parseInt(ver2[2]); } else { ver2 = [0,0,0]; } if(ver1[0] > ver2[0]) return -1; else if(ver1[0] < ver2[0]) return 1; if(ver1[1] > ver2[1]) return -1; else if(ver1[1] < ver2[1]) return 1; if(ver1[2] > ver2[2]) return -1; else if(ver1[2] < ver2[2]) return 1; return 0; } function checkVersion(name, component) { let msg = ''; if(rpack_map[name]) { let old_version = rpack_map[name].version; if(!old_version || old_version == '') { msg = ` '${name}' Upgrade (V0.0 -> V${component.version})`; } else { let c = versionCompare(old_version, component.version); if(c < 0) { msg = ` '${name}' Downgrade (V${old_version} -> V${component.version})`; } else if(c > 0) { msg = ` '${name}' Upgrade (V${old_version} -> V${component.version})`; } else { msg = ` '${name}' Same version (V${component.version})`; } } } else { msg = `'${name}' NEW (V${component.version})`; } return msg; } function handle_import_components(components) { let msg = 'Components:\n'; let cnt = 0; for(let name in components) { let component = components[name]; let v = checkVersion(name, component); if(cnt < 10) { msg += v + '\n'; } else if (cnt == 10) { msg += '...\n'; } else { // do nothing } cnt++; } let last_name = null; msg += '\nWill you load components?\n'; if(confirm(msg)) { let mode = confirm('\nWill you save components?\n(cancel=load without save)'); for(let name in components) { let component = components[name]; import_component(name, component, mode); last_name = name; } if(mode) { show_message('Components are saved.'); } else { show_message('Components are loaded.'); } } if(cnt == 1 && last_name) { const node = LiteGraph.createNode(`workflow/${last_name}`); node.pos = [app.canvas.graph_mouse[0], app.canvas.graph_mouse[1]]; app.canvas.graph.add(node, false); } } function handlePaste(e) { let data = (e.clipboardData || window.clipboardData); const items = data.items; for(const item of items) { if(item.kind == 'string' && item.type == 'text/plain') { data = data.getData("text/plain"); try { let json_data = JSON.parse(data); if(json_data.kind == 'ComfyUI Components' && last_paste_timestamp != json_data.timestamp) { last_paste_timestamp = json_data.timestamp; handle_import_components(json_data.components); // disable paste node localStorage.removeItem("litegrapheditor_clipboard", null); } else { console.log('This components are already pasted: ignored'); } } catch { // nothing to do } } } } document.addEventListener("paste", handlePaste); export class ComponentBuilderDialog extends ComfyDialog { constructor() { super(); } clear() { while (this.element.children.length) { this.element.removeChild(this.element.children[0]); } } show() { this.invalidateControl(); this.element.style.display = "block"; this.element.style.zIndex = 10001; this.element.style.width = "500px"; this.element.style.height = "480px"; } invalidateControl() { this.clear(); let self = this; const close_button = $el("button", { id: "cm-close-button", type: "button", textContent: "Close", onclick: () => self.close() }); this.save_button = $el("button", { id: "cm-save-button", type: "button", textContent: "Save", onclick: () => { save_as_component(self.target_node, self.version_string.value.trim(), self.author.value.trim(), self.node_prefix.value.trim(), self.getNodeName(), self.getPackName(), self.category.value.trim()); } }); let default_nodename = getPureName(this.target_node).trim(); let groupNode = app.graph.extra.groupNodes[default_nodename]; let default_packname = groupNode.packname; if(!default_packname) { default_packname = ''; } let default_category = groupNode.category; if(!default_category) { default_category = ''; } this.default_ver = groupNode.version; if(!this.default_ver) { this.default_ver = '0.0'; } let default_author = groupNode.author; if(!default_author) { default_author = ''; } let delimiterIndex = default_nodename.indexOf('::'); let default_prefix = ""; if(delimiterIndex != -1) { default_prefix = default_nodename.substring(0, delimiterIndex); default_nodename = default_nodename.substring(delimiterIndex + 2); } if(!default_prefix) { this.save_button.disabled = true; } this.pack_list = this.createPackListCombo(); let version_string = this.createLabeledInput('input version (e.g. 1.0)', '*Version : ', this.default_ver); this.version_string = version_string[1]; this.version_string.disabled = true; let author = this.createLabeledInput('input author (e.g. Dr.Lt.Data)', 'Author : ', default_author); this.author = author[1]; let node_prefix = this.createLabeledInput('input node prefix (e.g. mypack)', '*Prefix : ', default_prefix); this.node_prefix = node_prefix[1]; let manual_nodename = this.createLabeledInput('input node name (e.g. MAKE_BASIC_PIPE)', 'Nodename : ', default_nodename); this.manual_nodename = manual_nodename[1]; let manual_packname = this.createLabeledInput('input pack name (e.g. mypack)', 'Packname : ', default_packname); this.manual_packname = manual_packname[1]; let category = this.createLabeledInput('input category (e.g. util/pipe)', 'Category : ', default_category); this.category = category[1]; this.node_label = this.createNodeLabel(); let author_mode = this.createAuthorModeCheck(); this.author_mode = author_mode[0]; const content = $el("div.comfy-modal-content", [ $el("tr.cm-title", {}, [ $el("font", {size:6, color:"white"}, [`ComfyUI-Manager: Component Builder`])] ), $el("br", {}, []), $el("div.cm-menu-container", [ author_mode[0], author_mode[1], category[0], author[0], node_prefix[0], manual_nodename[0], manual_packname[0], version_string[0], this.pack_list, $el("br", {}, []), this.node_label ]), $el("br", {}, []), this.save_button, close_button, ] ); content.style.width = '100%'; content.style.height = '100%'; this.element = $el("div.comfy-modal", { id:'cm-manager-dialog', parent: document.body }, [ content ]); } validateInput() { let msg = ""; if(!isValidVersionString(this.version_string.value)) { msg += 'Invalid version string: '+event.value+"\n"; } if(this.node_prefix.value.trim() == '') { msg += 'Node prefix cannot be empty\n'; } if(this.manual_nodename.value.trim() == '') { msg += 'Node name cannot be empty\n'; } if(msg != '') { // alert(msg); } this.save_button.disabled = msg != ""; } getPackName() { if(this.pack_list.selectedIndex == 0) { return this.manual_packname.value.trim(); } return this.pack_list.value.trim(); } getNodeName() { if(this.manual_nodename.value.trim() != '') { return this.manual_nodename.value.trim(); } return getPureName(this.target_node); } createAuthorModeCheck() { let check = $el("input",{type:'checkbox', id:"author-mode"},[]) const check_label = $el("label",{for:"author-mode"},["Enable author mode"]); check_label.style.color = "var(--fg-color)"; check_label.style.cursor = "pointer"; check.checked = false; let self = this; check.onchange = () => { self.version_string.disabled = !check.checked; if(!check.checked) { self.version_string.value = self.default_ver; } else { alert('If you are not the author, it is not recommended to change the version, as it may cause component update issues.'); } }; return [check, check_label]; } createNodeLabel() { let label = $el('p'); label.className = 'cb-node-label'; if(this.target_node.comfyClass.includes('::')) label.textContent = getPureName(this.target_node); else label.textContent = " _::" + getPureName(this.target_node); return label; } createLabeledInput(placeholder, label, value) { let textbox = $el('input.cb-widget-input', {type:'text', placeholder:placeholder, value:value}, []); let self = this; textbox.onchange = () => { this.validateInput.call(self); this.node_label.textContent = this.node_prefix.value + "::" + this.manual_nodename.value; } let row = $el('span.cb-widget', {}, [ $el('span.cb-widget-input-label', label), textbox]); return [row, textbox]; } createPackListCombo() { let combo = document.createElement("select"); combo.className = "cb-widget"; let default_packname_option = { value: '##manual', text: 'Packname: Manual' }; combo.appendChild($el('option', default_packname_option, [])); for(let name in pack_map) { combo.appendChild($el('option', { value: name, text: 'Packname: '+ name }, [])); } let self = this; combo.onchange = function () { if(combo.selectedIndex == 0) { self.manual_packname.disabled = false; } else { self.manual_packname.disabled = true; } }; return combo; } } let orig_handleFile = app.handleFile; function handleFile(file) { if (file.name?.endsWith(".json") || file.name?.endsWith(".pack")) { const reader = new FileReader(); reader.onload = async () => { let is_component = false; const jsonContent = JSON.parse(reader.result); for(let name in jsonContent) { let cand = jsonContent[name]; is_component = cand.datetime && cand.version; break; } if(is_component) { handle_import_components(jsonContent); } else { orig_handleFile.call(app, file); } }; reader.readAsText(file); return; } orig_handleFile.call(app, file); } app.handleFile = handleFile; let current_component_policy = 'workflow'; try { api.fetchApi('/manager/component/policy') .then(response => response.text()) .then(data => { current_component_policy = data; }); } catch {} function getChangedVersion(groupNodes) { if(!Object.keys(pack_map).length || !groupNodes) return null; let res = {}; for(let component_name in groupNodes) { let data = groupNodes[component_name]; if(rpack_map[component_name]) { let v = versionCompare(data.version, rpack_map[component_name].version); res[component_name] = v; } } return res; } const loadGraphData = app.loadGraphData; app.loadGraphData = async function () { if(arguments.length == 0) return await loadGraphData.apply(this, arguments); let graphData = arguments[0]; let groupNodes = graphData.extra?.groupNodes; let res = getChangedVersion(groupNodes); if(res) { let target_components = null; switch(current_component_policy) { case 'higher': target_components = Object.keys(res).filter(key => res[key] == 1); break; case 'mine': target_components = Object.keys(res); break; default: // do nothing } if(target_components) { for(let i in target_components) { let component_name = target_components[i]; let component = rpack_map[component_name]; if(component && graphData.extra?.groupNodes) { graphData.extra.groupNodes[component_name] = component; } } } } else { console.log('Empty components: policy ignored'); } arguments[0] = graphData; return await loadGraphData.apply(this, arguments); }; export function set_component_policy(v) { current_component_policy = v; } let graphToPrompt = app.graphToPrompt; app.graphToPrompt = async function () { let p = await graphToPrompt.call(app); try { let groupNodes = p.workflow.extra?.groupNodes; if(groupNodes) { p.workflow.extra = { ... p.workflow.extra}; // get used group nodes let used_group_nodes = new Set(); for(let node of p.workflow.nodes) { if(node.type.startsWith('workflow/')) { used_group_nodes.add(node.type.substring(9)); } } // remove unused group nodes let new_groupNodes = {}; for (let key in p.workflow.extra.groupNodes) { if (used_group_nodes.has(key)) { new_groupNodes[key] = p.workflow.extra.groupNodes[key]; } } p.workflow.extra.groupNodes = new_groupNodes; } } catch(e) { console.log(`Failed to filtering group nodes: ${e}`); } return p; }