import { app } from "../../scripts/app.js"; import { api } from "../../scripts/api.js"; // import { LGraphNode } from "../../scripts/widgets.js"; LGraphNode = LiteGraph.LGraphNode; import { ComfyDialog, $el } from "../../scripts/ui.js"; import { initializeWorkflowsList, addWorkflowSearch } from "./workflow-list.js"; import { initializeMachineManager } from "./machine-manager.js"; import { fetchSnapshotSimple } from "./snapshot-utils.js"; import { generateDependencyGraph } from "https://esm.sh/comfyui-json@0.1.25"; // import { ComfyDeploy } from "https://esm.sh/comfydeploy@2.0.0-beta.69"; const styles = ` .comfydeploy-menu-item { background: linear-gradient(to right, rgba(74, 144, 226, 0.9), rgba(103, 178, 111, 0.9)) !important; color: white !important; position: relative !important; padding-left: 20px !important; } .comfydeploy-menu-item:hover { filter: brightness(1.1) !important; cursor: pointer !important; } .comfydeploy-menu-item::before { content: ''; position: absolute; left: 4px; top: 50%; transform: translateY(-50%); width: 12px; height: 12px; background-image: url('https://www.comfydeploy.com/icon.svg'); background-size: contain; background-repeat: no-repeat; background-position: center; } `; // Add stylesheet to document const styleSheet = document.createElement("style"); styleSheet.textContent = styles; document.head.appendChild(styleSheet); const loadingIcon = ``; function sendEventToCD(event, data) { const message = { type: event, data: data, }; window.parent.postMessage(JSON.stringify(message), "*"); } function sendDirectEventToCD(event, data) { const message = { type: event, data: data, }; window.parent.postMessage(message, "*"); } function dispatchAPIEventData(data) { const msg = JSON.parse(data); // Custom parse error if (msg.error) { let message = msg.error.message; if (msg.error.details) message += ": " + msg.error.details; for (const [nodeID, nodeError] of Object.entries(msg.node_errors)) { message += "\n" + nodeError.class_type + ":"; for (const errorReason of nodeError.errors) { message += "\n - " + errorReason.message + ": " + errorReason.details; } } app.ui.dialog.show(message); if (msg.node_errors) { app.lastNodeErrors = msg.node_errors; app.canvas.draw(true, true); } } switch (msg.event) { case "error": break; case "status": if (msg.data.sid) { // this.clientId = msg.data.sid; // window.name = this.clientId; // use window name so it isnt reused when duplicating tabs // sessionStorage.setItem("clientId", this.clientId); // store in session storage so duplicate tab can load correct workflow } api.dispatchEvent(new CustomEvent("status", { detail: msg.data.status })); break; case "progress": api.dispatchEvent(new CustomEvent("progress", { detail: msg.data })); break; case "executing": api.dispatchEvent( new CustomEvent("executing", { detail: msg.data.node }) ); break; case "executed": api.dispatchEvent(new CustomEvent("executed", { detail: msg.data })); break; case "execution_start": api.dispatchEvent( new CustomEvent("execution_start", { detail: msg.data }) ); break; case "execution_error": api.dispatchEvent( new CustomEvent("execution_error", { detail: msg.data }) ); break; case "execution_cached": api.dispatchEvent( new CustomEvent("execution_cached", { detail: msg.data }) ); break; default: api.dispatchEvent(new CustomEvent(msg.type, { detail: msg.data })); // default: // if (this.#registered.has(msg.type)) { // } else { // throw new Error(`Unknown message type ${msg.type}`); // } } } const context = { selectedWorkflowInfo: null, }; // let selectedWorkflowInfo = { // workflow_id: "05da8f2b-63af-4c0c-86dd-08d01ec512b7", // machine_id: "45ac5f85-b7b6-436f-8d97-2383b25485f3", // native_run_api_endpoint: "http://localhost:3011/api/run", // }; async function getSelectedWorkflowInfo() { const workflow_info_promise = new Promise((resolve) => { try { const handleMessage = (event) => { try { const message = JSON.parse(event.data); if (message.type === "workflow_info") { resolve(message.data); window.removeEventListener("message", handleMessage); } } catch (error) { console.error(error); resolve(undefined); } }; window.addEventListener("message", handleMessage); sendEventToCD("workflow_info"); } catch (error) { console.error(error); resolve(undefined); } }); return workflow_info_promise; } function setSelectedWorkflowInfo(info) { context.selectedWorkflowInfo = info; } const VALID_TYPES = [ "STRING", "number", "toggle", "BOOLEAN", "text", "string", "combo", ]; function hideWidget(node, widget, suffix = "") { if (widget.type?.startsWith(CONVERTED_TYPE)) return; widget.origType = widget.type; widget.origComputeSize = widget.computeSize; widget.origSerializeValue = widget.serializeValue; // console.log(widget.origComputeSize); // console.log(LiteGraph.NODE_SLOT_HEIGHT); // widget.computeSize = () => [0, 0]; widget.type = CONVERTED_TYPE + suffix; widget.serializeValue = () => { if (!node.inputs) { return void 0; } let node_input = node.inputs.find((i) => i.widget?.name === widget.name); if (!node_input || !node_input.link) { return void 0; } return widget.origSerializeValue ? widget.origSerializeValue() : widget.value; }; if (widget.linkedWidgets) { for (const w of widget.linkedWidgets) { hideWidget(node, w, ":" + widget.name); } } } function getWidgetType(config) { let type = config[0]; if (type instanceof Array) { type = "COMBO"; } return { type }; } async function convertToInput(node, widget, config) { const { type } = getWidgetType(config); console.log(node, widget, config); const result = await app.extensionManager.dialog.prompt({ title: "Convert " + widget.name + " to external input", message: "Input name", defaultValue: widget.name, }); if (!result) return; // Check for duplicate input IDs across existing external input nodes const existingInputIds = Object.values(app.graph.nodes) .filter((n) => n.type.startsWith("ComfyUIDeployExternal")) .map((n) => n.widgets_values?.[0]) .filter(Boolean); if (existingInputIds.includes(result)) { app.extensionManager.toast.add({ severity: "error", summary: "Input ID already exists", detail: "Please choose a different name.", life: 3000, }); return; } if (node.type == "LoadImage") { var inputNode = LiteGraph.createNode("ComfyUIDeployExternalImage"); console.log(widget); const currentOutputsLinks = node.outputs[0].links; // const index = node.inputs.findIndex((x) => x.name == widget.name); // console.log(node.widgets_values, index); // inputNode.configure({ // widgets_values: ["input_text", widget.value], // }); inputNode.pos = node.pos; inputNode.id = ++app.graph.last_node_id; // inputNode.pos[0] += node.size[0] + 40; node.pos[0] -= inputNode.size[0] + 20; console.log(inputNode); console.log(app.graph); app.graph.add(inputNode); const links = app.graph.links; // console.log(currentOutputsLinks); if (currentOutputsLinks) for (let i = 0; i < currentOutputsLinks.length; i++) { const link = currentOutputsLinks[i]; const llink = links[link]; console.log(links[link]); setTimeout( () => inputNode.connect(0, llink.target_id, llink.target_slot), 100 ); } node.connect(0, inputNode, 0); return null; } let externalNode = ""; let inputId = result; if (type === "INT") { externalNode = "ComfyUIDeployExternalNumberInt"; // inputId = "input_number"; } if (type === "FLOAT") { externalNode = "ComfyUIDeployExternalNumberSlider"; // inputId = "input_number"; } if (type === "STRING") { externalNode = "ComfyUIDeployExternalText"; // inputId = "input_text"; } if (type === "COMBO") { externalNode = "ComfyUIDeployExternalEnum"; // inputId = "input_enum"; } if (!externalNode || !inputId) return; node.convertWidgetToInput(widget); var inputNode = LiteGraph.createNode( externalNode, "External Input: " + inputId ); // if (type === "COMBO") { // inputNode = LiteGraph.createNode(externalNode, "External Input: " + inputId, { // dynamic_enum_options: config[0], // }); // console.log(inputNode); // const options = config[0]; // console.log(options); // } else { // inputNode = LiteGraph.createNode(externalNode, "External Input: " + inputId); // } var options; const index = node.inputs.findIndex((x) => x.name == widget.name); if (type === "COMBO") { options = widget.options?.values ?? config[0]; inputNode.configure({ widgets_values: [inputId, widget.value, JSON.stringify(options)], }); } else { inputNode.configure({ widgets_values: [inputId, widget.value], }); } inputNode.id = ++app.graph.last_node_id; inputNode.pos = node.pos; inputNode.pos[0] -= node.size[0] + 160; if (type === "COMBO") { console.log(inputNode); console.log(options); inputNode.widgets.find((x) => x.name == "default_value").options.values = options; } app.graph.add(inputNode); inputNode.connect(0, node, index); app.graph.setDirtyCanvas(true, true); return node.inputs.find((x) => x.name == widget.name); } const CONVERTED_TYPE = "converted-widget"; function getConfig(widgetName) { const { nodeData } = this.constructor; return ( nodeData?.input?.required?.[widgetName] ?? nodeData?.input?.optional?.[widgetName] ); } function isConvertibleWidget(node, widget, config) { // console.log(config); if ( node.type === "LoadImage" && widget.type === "combo" && widget.name == "image" ) { return true; } return ( (VALID_TYPES.includes(widget.type) || VALID_TYPES.includes(config[0])) && !widget.options?.forceInput ); } var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); /** @typedef {import('../../../web/types/comfy.js').ComfyExtension} ComfyExtension*/ /** @type {ComfyExtension} */ const ext = { name: "BennyKok.ComfyUIDeploy", native_mode: false, init(app) { addButton(); const queryParams = new URLSearchParams(window.location.search); const workflow_version_id = queryParams.get("workflow_version_id"); const auth_token = queryParams.get("auth_token"); const org_display = queryParams.get("org_display"); const origin = queryParams.get("origin"); const workspace_mode = queryParams.get("workspace_mode"); this.native_mode = queryParams.get("native_mode") === "true"; if (workspace_mode) { document.querySelector(".comfy-menu").style.display = "none"; sendEventToCD("cd_plugin_onInit"); // app.queuePrompt = ((originalFunction) => async () => { // // const prompt = await app.graphToPrompt(); // sendEventToCD("cd_plugin_onQueuePromptTrigger"); // })(app.queuePrompt); // // Intercept the onkeydown event // window.addEventListener( // "keydown", // (event) => { // // Check for specific keys if necessary // console.log("hi"); // if ((event.metaKey || event.ctrlKey) && event.key === "Enter") { // event.preventDefault(); // event.stopImmediatePropagation(); // event.stopPropagation(); // sendEventToCD("cd_plugin_onQueuePrompt", prompt); // } // }, // true, // ); } const data = getData(); let endpoint = data.endpoint; let apiKey = data.apiKey; // If there is auth token override it if (auth_token) { apiKey = auth_token; endpoint = origin; saveData({ displayName: org_display, endpoint: origin, apiKey: auth_token, displayName: org_display, environment: "cloud", }); localStorage.setItem("comfy_deploy_env", "cloud"); } if (!workflow_version_id) { console.error("No workflow_version_id provided in query parameters."); } else { loadingDialog.showLoading( "Loading workflow from " + org_display, "Please wait..." ); fetch(endpoint + "/api/workflow-version/" + workflow_version_id, { method: "GET", headers: { "Content-Type": "application/json", Authorization: "Bearer " + apiKey, }, }) .then(async (res) => { const data = await res.json(); const { workflow, workflow_id, error } = data; if (error) { infoDialog.showMessage("Unable to load this workflow", error); return; } // // Adding a delay to wait for the intial graph to load // await new Promise((resolve) => setTimeout(resolve, 2000)); workflow?.nodes.forEach((x) => { if (x?.type === "ComfyDeploy") { x.widgets_values[1] = workflow_id; // x.widgets_values[2] = workflow_version.version; } }); /** @type {LGraph} */ app.loadGraphData(workflow); }) .catch((e) => infoDialog.showMessage("Error", e.message)) .finally(() => { loadingDialog.close(); window.history.replaceState( {}, document.title, window.location.pathname ); }); } }, async beforeRegisterNodeDef(nodeType, nodeData, app2) { const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions; nodeType.prototype.getExtraMenuOptions = function (_, options) { const r = origGetExtraMenuOptions ? origGetExtraMenuOptions.apply(this, arguments) : void 0; if (this.widgets) { let toInput = []; let toWidget = []; for (const w of this.widgets) { if (w.options?.forceInput) { continue; } if (w.type === CONVERTED_TYPE) { toWidget.push({ content: `Convert ${w.name} to widget`, callback: /* @__PURE__ */ __name( () => convertToWidget(this, w), "callback" ), }); } else { const config = getConfig.call(this, w.name) ?? [ w.type, w.options || {}, ]; if (isConvertibleWidget(this, w, config)) { toInput.push({ content: `Convert ${w.name} to external input`, callback: /* @__PURE__ */ __name( async () => convertToInput(this, w, config), "callback" ), className: "comfydeploy-menu-item", }); } } } if (toInput.length) { if (true) { options.push(); let optionIndex = options.findIndex((o) => o.content === "Outputs"); if (optionIndex === -1) optionIndex = options.length; else optionIndex++; options.splice( 0, 0, // { // content: "[ComfyDeploy] Convert to External Input", // submenu: { // options: toInput, // }, // className: "comfydeploy-menu-item" // }, ...toInput, null ); } else { options.push(...toInput, null); } } // if (toWidget.length) { // if (useConversionSubmenusSetting.value) { // options.push({ // content: "Convert Input to Widget", // submenu: { // options: toWidget, // }, // }); // } else { // options.push(...toWidget, null); // } // } } return r; }; if ( nodeData?.input?.optional?.default_value_url?.[1]?.image_preview === true ) { nodeData.input.optional.default_value_url = ["IMAGEPREVIEW"]; console.log(nodeData.input.optional.default_value_url); } if (nodeData?.input?.optional?.default_value?.[1]?.dynamic_enum === true) { nodeData.input.optional.default_value = ["DYNAMIC_ENUM"]; // console.log(nodeData.input.optional.default_value); } // const origonNodeCreated = nodeType.prototype.onNodeCreated; // nodeType.prototype.onNodeCreated = function () { // const r = origonNodeCreated // ? origonNodeCreated.apply(this, arguments) // : void 0; // if (!this.widgets) { // return; // } // console.log(this.widgets); // this.widgets.forEach(element => { // if (element.type != "customtext") return // console.log(element.element); // const parent = element.element.parentElement // console.log(element.element.parentElement) // const btn = document.createElement("button"); // // const div = document.createElement("div"); // // parent.removeChild(element.element) // // div.appendChild(element.element) // // parent.appendChild(div) // // element.element = div // // console.log(element.element); // // btn.style = element.element.style // }); // return r // }; }, // async nodeCreated(node) { // }, registerCustomNodes() { /** @type {LGraphNode}*/ class ComfyDeploy extends LGraphNode { constructor() { super(); this.color = LGraphCanvas.node_colors.yellow.color; this.bgcolor = LGraphCanvas.node_colors.yellow.bgcolor; this.groupcolor = LGraphCanvas.node_colors.yellow.groupcolor; if (!this.properties) { this.properties = {}; this.properties.workflow_name = ""; this.properties.workflow_id = ""; this.properties.version = ""; } this.addWidget( "text", "workflow_name", this.properties.workflow_name, (v) => { this.properties.workflow_name = v; }, { multiline: false } ); this.addWidget( "text", "workflow_id", this.properties.workflow_id, (v) => { this.properties.workflow_id = v; }, { multiline: false } ); this.addWidget( "text", "version", this.properties.version, (v) => { this.properties.version = v; }, { multiline: false } ); this.widgets_start_y = 10; this.serialize_widgets = true; this.isVirtualNode = true; } onExecute() { // This method is called when the node is executed // You can add any necessary logic here } onSerialize(o) { // This method is called when the node is being serialized // Ensure all necessary data is saved if (!o.properties) { o.properties = {}; } o.properties.workflow_name = this.properties.workflow_name; o.properties.workflow_id = this.properties.workflow_id; o.properties.version = this.properties.version; } onConfigure(o) { // This method is called when the node is being configured (e.g., when loading a saved graph) // ComfyUI stores widget values in widgets_values array, not properties if (o.widgets_values && o.widgets_values.length >= 3) { // Set the widget values directly this.widgets[0].value = o.widgets_values[0] || ""; this.widgets[1].value = o.widgets_values[1] || ""; this.widgets[2].value = o.widgets_values[2] || "1"; // Also update properties to stay in sync this.properties.workflow_name = o.widgets_values[0] || ""; this.properties.workflow_id = o.widgets_values[1] || ""; this.properties.version = o.widgets_values[2] || "1"; } else if (o.properties) { // Fallback to properties if widgets_values is not available this.properties = { ...this.properties, ...o.properties }; this.widgets[0].value = this.properties.workflow_name || ""; this.widgets[1].value = this.properties.workflow_id || ""; this.widgets[2].value = this.properties.version || "1"; } } } // Register the node type LiteGraph.registerNodeType( "ComfyDeploy", Object.assign(ComfyDeploy, { title: "Comfy Deploy", title_mode: LiteGraph.NORMAL_TITLE, collapsable: true, }) ); ComfyDeploy.category = "deploy"; }, getCustomWidgets() { return { IMAGEPREVIEW(node, inputName, inputData) { // Find or create the URL input widget const urlWidget = node.addWidget( "string", inputName, /* value=*/ "", () => {}, { serialize: true } ); const buttonWidget = node.addWidget( "button", "Open Assets Browser", /* value=*/ "", () => { sendEventToCD("assets", { node: node.id, inputName: inputName, }); // console.log("load image"); }, { serialize: false } ); console.log(node.widgets); console.log("urlWidget", urlWidget); // Add image preview functionality function showImage(url) { const img = new Image(); img.onload = () => { node.imgs = [img]; app.graph.setDirtyCanvas(true); node.setSizeForImage?.(); }; img.onerror = () => { node.imgs = []; app.graph.setDirtyCanvas(true); }; img.src = url; } // Set up URL widget value handling let default_value = urlWidget.value; Object.defineProperty(urlWidget, "value", { set: function (value) { this._real_value = value; // Preview image when URL changes if (value) { showImage(value); } }, get: function () { return this._real_value || default_value; }, }); // Show initial image if URL exists requestAnimationFrame(() => { if (urlWidget.value) { showImage(urlWidget.value); } }); return { widget: urlWidget }; }, DYNAMIC_ENUM(node, inputName, inputData) { // console.log("DYNAMIC_ENUM", JSON.parse(JSON.stringify(node)), inputName, inputData); const enumWidget = node.addWidget("combo", inputName, "", { serialize: true, values: [], }); return { widget: enumWidget }; }, }; }, async afterConfigureGraph() { app.graph.nodes.forEach((node) => { if (node.type === "ComfyUIDeployExternalEnum") { const default_value_index = node.widgets.findIndex( (x) => x.name === "default_value" ); const options_index = node.widgets.findIndex( (x) => x.name === "options" ); var dynamic_enum_options = [node.widgets[default_value_index].value]; if (node.widgets[options_index].value) { dynamic_enum_options = JSON.parse(node.widgets[options_index].value); } // console.log("dynamic_enum_options", dynamic_enum_options); node.widgets[default_value_index].options.values = dynamic_enum_options; } }); }, async setup() { // const graphCanvas = document.getElementById("graph-canvas"); window.addEventListener("message", async (event) => { // console.log("message", event); try { const message = JSON.parse(event.data); if (message.type === "graph_load") { const comfyUIWorkflow = message.data; // console.log("recieved: ", comfyUIWorkflow); // Assuming there's a method to load the workflow data into the ComfyUI // This part of the code would depend on how the ComfyUI expects to receive and process the workflow data // For demonstration, let's assume there's a loadWorkflow method in the ComfyUI API if (comfyUIWorkflow && app && app.loadGraphData) { try { await window["app"].ui.settings.setSettingValueAsync( "Comfy.Validation.Workflows", true ); } catch (error) { console.warning( "Error setting validation to false, is fine to ignore this", error ); } console.log("loadGraphData"); app.loadGraphData(comfyUIWorkflow); sendEventToCD("graph_loaded"); } } else if (message.type === "deploy") { // deployWorkflow(); const prompt = await app.graphToPrompt(); // api.handlePromptGenerated(prompt); sendEventToCD("cd_plugin_onDeployChanges", prompt); } else if (message.type === "queue_prompt") { const prompt = await app.graphToPrompt(); if (typeof api.handlePromptGenerated === "function") { api.handlePromptGenerated(prompt); } else { console.warn("api.handlePromptGenerated is not a function"); } sendEventToCD("cd_plugin_onQueuePrompt", prompt); } else if (message.type === "configure_queue_buttons") { addQueueButtons(message.data); } else if (message.type === "configure_menu_right_buttons") { addMenuRightButtons(message.data); } else if (message.type === "configure_menu_buttons") { addMenuButtons(message.data); } else if (message.type === "get_prompt") { const prompt = await app.graphToPrompt(); sendEventToCD("cd_plugin_onGetPrompt", prompt); } else if (message.type === "event") { dispatchAPIEventData(message.data); } else if (message.type === "update_widget") { // New handler for updating widget values const { nodeId, widgetName, value } = message.data; const node = app.graph.getNodeById(nodeId); if (!node) { console.warn(`Node with ID ${nodeId} not found`); return; } const widget = node.widgets?.find((w) => w.name === widgetName); if (!widget) { console.warn(`Widget ${widgetName} not found in node ${nodeId}`); return; } widget.value = value; app.graph.setDirtyCanvas(true); } else if (message.type === "add_node") { console.log("add node", message.data); app.graph.beforeChange(); var node = LiteGraph.createNode(message.data.type); node.configure({ widgets_values: message.data.widgets_values, }); console.log("node", node); const graphMouse = app.canvas.graph_mouse; node.pos = [graphMouse[0], graphMouse[1]]; app.graph.add(node); app.graph.afterChange(); } else if (message.type === "zoom_to_node") { const nodeId = message.data.nodeId; const position = message.data.position; const node = app.graph.getNodeById(nodeId); if (!node) return; const canvas = app.canvas; const targetScale = 1; const targetOffsetX = canvas.canvas.width / 4 - position[0] - node.size[0] / 2; const targetOffsetY = canvas.canvas.height / 4 - position[1] - node.size[1] / 2; const startScale = canvas.ds.scale; const startOffsetX = canvas.ds.offset[0]; const startOffsetY = canvas.ds.offset[1]; const duration = 400; // Animation duration in milliseconds const startTime = Date.now(); function easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); } function lerp(start, end, t) { return start * (1 - t) + end * t; } function animate() { const currentTime = Date.now(); const elapsedTime = currentTime - startTime; const t = Math.min(elapsedTime / duration, 1); const easedT = easeOutCubic(t); const currentScale = lerp(startScale, targetScale, easedT); const currentOffsetX = lerp(startOffsetX, targetOffsetX, easedT); const currentOffsetY = lerp(startOffsetY, targetOffsetY, easedT); canvas.setZoom(currentScale); canvas.ds.offset = [currentOffsetX, currentOffsetY]; canvas.draw(true, true); if (t < 1) { requestAnimationFrame(animate); } } animate(); } else if (message.type === "workflow_info") { setSelectedWorkflowInfo(message.data); } // else if (message.type === "refresh") { // sendEventToCD("cd_plugin_onRefresh"); // } } catch (error) { // console.error("Error processing message:", error); } }); api.addEventListener("executed", (evt) => { const images = evt.detail?.output.images; // if (images?.length > 0 && images[0].type === "output") { // generatedImages[evt.detail.node] = images[0].filename; // } // if (evt.detail?.output.gltfFilename) { // } }); if (this.native_mode) { // console.log("native mode", window, window.app); try { await app.ui.settings.setSettingValueAsync("Comfy.UseNewMenu", "Top"); await app.ui.settings.setSettingValueAsync( "Comfy.Sidebar.Size", "small" ); await app.ui.settings.setSettingValueAsync( "Comfy.Sidebar.Location", "left" ); await app.ui.settings.setSettingValueAsync( "Comfy.TutorialCompleted", true ); console.log("native mode manmanman"); } catch (error) { console.error("Error setting validation to false", error); } } app.graph.onAfterChange = ((originalFunction) => async function () { const prompt = await app.graphToPrompt(); sendEventToCD("cd_plugin_onAfterChange", prompt); if (typeof originalFunction === "function") { originalFunction.apply(this, arguments); } })(app.graph.onAfterChange); sendEventToCD("cd_plugin_setup"); }, }; /** * @typedef {import('../../../web/types/litegraph.js').LGraph} LGraph * @typedef {import('../../../web/types/litegraph.js').LGraphNode} LGraphNode */ function showError(title, message) { infoDialog.show( `

${title}


${message} ` ); } function createDynamicUIHtml(data) { console.log(data); let html = '
'; const bgcolor = "var(--comfy-input-bg)"; const evenBg = "var(--border-color)"; const textColor = "var(--input-text)"; // Custom Nodes html += `
`; html += '

Custom Nodes

'; if (data.missing_nodes?.length > 0) { html += `

Missing Nodes

These nodes are not found with any matching custom_nodes in the ComfyUI Manager Database

${data.missing_nodes .map((node) => { return `

${node}

`; }) .join("")}
`; } Object.values(data.custom_nodes).forEach((node) => { html += `
${ node.name }

${node.hash}

${ node.warning ? `

${node.warning}

` : "" }
`; }); html += "
"; // Models html += `
`; html += '

Models

'; Object.entries(data.models).forEach(([section, items]) => { html += `

${ section.charAt(0).toUpperCase() + section.slice(1) }

`; items.forEach((item) => { html += `

${item.name}

`; }); html += "
"; }); html += "
"; // Models html += `
`; html += '

Files

'; Object.entries(data.files).forEach(([section, items]) => { html += `

${ section.charAt(0).toUpperCase() + section.slice(1) }

`; items.forEach((item) => { html += `

${item.name}

`; }); html += "
"; }); html += "
"; html += "
"; return html; } // Modify the existing deployWorkflow function async function deployWorkflow() { const deploy = document.getElementById("deploy-button"); /** @type {LGraph} */ const graph = app.graph; let { endpoint, apiKey, apiUrl, displayName } = getData(); if (!endpoint || !apiKey || apiKey === "" || endpoint === "" || !apiUrl) { configDialog.show(); return; } let deployMeta = graph.findNodesByType("ComfyDeploy"); if (deployMeta.length == 0) { const text = await inputDialog.input( "Create your deployment", "Workflow name" ); if (!text) return; console.log(text); app.graph.beforeChange(); var node = LiteGraph.createNode("ComfyDeploy"); node.configure({ widgets_values: [text], }); node.pos = [0, 0]; app.graph.add(node); app.graph.afterChange(); deployMeta = [node]; } const deployMetaNode = deployMeta[0]; const workflow_name = deployMetaNode.widgets[0].value; const workflow_id = deployMetaNode.widgets[1].value; const ok = await confirmDialog.confirm( `Confirm deployment`, `
A new version of will be deployed, do you confirm?


` ); if (!ok) return; const prompt = await app.graphToPrompt(); console.log(prompt); if (workflow_id.trim() !== "") { const text = await inputDialog.input("Save changes", "Comment"); if (!text) return; try { loadingDialog.showLoading("Saving changes"); const body = { api_url: apiUrl, workflow: prompt.workflow, workflow_id: workflow_id, workflow_api: prompt.output, comment: text, }; let data = await fetch("/comfyui-deploy/workflow/version", { method: "POST", body: JSON.stringify(body), headers: { "Content-Type": "application/json", Authorization: "Bearer " + apiKey, }, }); if (data.status !== 200) { throw new Error(await data.text()); } else { data = await data.json(); console.log(data); infoDialog.show( `New version created!

Workflow ID: ${data.workflow_id}
Workflow Name: ${workflow_name}
Workflow Version: ${data.version}
` ); deployMetaNode.widgets[2].value = data.version; graph.change(); } } catch (e) { infoDialog.showError("Error", e.message); return; } finally { loadingDialog.close(); return; } } if (endpoint.endsWith("/")) { endpoint = endpoint.slice(0, -1); } loadingDialog.showLoading("Generating snapshot"); const snapshot = await fetchSnapshotSimple(); console.log(snapshot); loadingDialog.close(); if (!snapshot) { showError( "Error when deploying", "Unable to generate snapshot, please install ComfyUI Manager" ); return; } const title = deploy.querySelector("#button-title"); loadingDialog.showLoading("Deploying..."); title.innerText = "Deploying..."; title.style.color = "orange"; try { const body = { name: workflow_name, workflow_json: prompt.workflow, workflow_api: prompt.output, api_url: apiUrl, }; const machineId = localStorage.getItem("comfy_deploy_machine_id"); if (machineId) { body.machine_id = machineId; } console.log(body); let data = await fetch("/comfyui-deploy/workflow", { method: "POST", body: JSON.stringify(body), headers: { "Content-Type": "application/json", Authorization: "Bearer " + apiKey, }, }); console.log(data); if (data.status !== 200) { throw new Error(await data.text()); } else { data = await data.json(); await refreshWorkflowListIfOpen(); } loadingDialog.close(); title.textContent = "Done"; title.style.color = "green"; deployMetaNode.widgets[1].value = data.workflow_id; deployMetaNode.widgets[2].value = 2; graph.change(); if (data.workflow_id) { const prompt_with_workflow_id = await app.graphToPrompt(); const body = { api_url: apiUrl, workflow: prompt_with_workflow_id.workflow, workflow_id: data.workflow_id, workflow_api: prompt_with_workflow_id.output, comment: "chore: apply workflow id", }; let new_version_data = await fetch("/comfyui-deploy/workflow/version", { method: "POST", body: JSON.stringify(body), headers: { "Content-Type": "application/json", Authorization: "Bearer " + apiKey, }, }); if (new_version_data.status !== 200) { throw new Error(await new_version_data.text()); } } infoDialog.show( `Deployed successfully! -> View here

Workflow ID: ${data.workflow_id}
Workflow Name: ${workflow_name}
Workflow Version: 2
` ); setTimeout(() => { title.textContent = "Deploy"; title.style.color = "white"; }, 1000); } catch (e) { loadingDialog.close(); app.ui.dialog.show(e); console.error(e); title.textContent = "Error"; title.style.color = "red"; setTimeout(() => { title.textContent = "Deploy"; title.style.color = "white"; }, 1000); } } function addButton() { const menu = document.querySelector(".comfy-menu"); const deploy = document.createElement("button"); deploy.id = "deploy-button"; deploy.style.position = "relative"; deploy.style.display = "block"; deploy.innerHTML = "
Deploy
"; deploy.onclick = async () => { await deployWorkflow(); }; const config = document.createElement("img"); // config.style.padding = "0px 10px"; config.style.height = "100%"; config.style.position = "absolute"; config.style.right = "10px"; config.style.top = "0px"; // set aspect ratio to square config.style.width = "20px"; config.src = "https://api.iconify.design/material-symbols-light:settings.svg?color=%23888888"; config.onclick = (e) => { e.preventDefault(); e.stopPropagation(); configDialog.show(); }; deploy.append(config); deploy.style.order = "99"; menu.append(deploy); } app.registerExtension(ext); export class InfoDialog extends ComfyDialog { constructor() { super(); this.element.classList.add("comfy-normal-modal"); this.element.style.paddingBottom = "20px"; } button = undefined; createButtons() { this.button = $el("button", { type: "button", textContent: "Close", onclick: () => this.close(), }); return [this.button]; } close() { this.element.style.display = "none"; } show(html) { this.textElement.style["white-space"] = "normal"; this.textElement.style.color = "white"; this.textElement.style.marginTop = "0px"; if (typeof html === "string") { this.textElement.innerHTML = html; } else { this.textElement.replaceChildren(html); } this.element.style.display = "flex"; this.element.style.zIndex = 1001; } showMessage(title, message) { this.show(`

${title}

`); } showLoading(title, message) { this.show(`

${title} ${loadingIcon}

`); } showError(title, message) { this.show(`

${title}

${message}
`); // Set higher z-index to appear above config dialog this.element.style.zIndex = 1010; } } export class LoadingDialog extends ComfyDialog { constructor() { super(); this.element.classList.add("comfy-normal-modal"); // this.element.style.paddingBottom = "20px"; } createButtons() { return []; } close() { this.element.style.display = "none"; } show(html) { this.textElement.style["white-space"] = "normal"; this.textElement.style.color = "white"; this.textElement.style.marginTop = "0px"; if (typeof html === "string") { this.textElement.innerHTML = html; } else { this.textElement.replaceChildren(html); } this.element.style.display = "flex"; this.element.style.zIndex = 1001; } loadingIcon = ``; showLoading(title, message) { this.show(`

${title} ${ this.loadingIcon }

${ message ? `` : "" }
`); } } export class InputDialog extends InfoDialog { callback = undefined; constructor() { super(); } createButtons() { return [ $el( "div", { type: "div", style: { display: "flex", gap: "6px", justifyContent: "flex-end", width: "100%", }, }, [ $el("button", { type: "button", textContent: "Close", onclick: () => { this.callback?.(undefined); this.close(); }, }), $el("button", { type: "button", textContent: "Save", onclick: () => { const input = this.textElement.querySelector("#input").value; if (input.trim() === "") { showError("Input validation", "Input cannot be empty"); } else { this.callback?.(input); this.close(); this.textElement.querySelector("#input").value = ""; } }, }), ] ), ]; } input(title, message) { return new Promise((resolve, reject) => { this.callback = resolve; this.show(`

${title}

`); }); } } export class ConfirmDialog extends InfoDialog { callback = undefined; constructor() { super(); } createButtons() { return [ $el( "div", { type: "div", style: { display: "flex", gap: "6px", justifyContent: "flex-end", width: "100%", }, }, [ $el("button", { type: "button", textContent: "Close", onclick: () => { this.callback?.(false); this.close(); }, }), $el("button", { type: "button", textContent: "Confirm", style: { color: "green", }, onclick: () => { this.callback?.(true); this.close(); }, }), ] ), ]; } confirm(title, message) { return new Promise((resolve, reject) => { this.callback = resolve; this.show(`

${title}

${message}
`); }); } } export const inputDialog = new InputDialog(); export const loadingDialog = new LoadingDialog(); export const infoDialog = new InfoDialog(); export const confirmDialog = new ConfirmDialog(); /** * Retrieves deployment data from local storage or defaults. * @param {string} [environment] - The environment to get the data for. * @returns {{endpoint: string, apiKey: string, displayName: string, environment?: string}} The deployment data. */ function getData(environment) { const deployOption = environment || localStorage.getItem("comfy_deploy_env") || "cloud"; const data = localStorage.getItem("comfy_deploy_env_data_" + deployOption); if (!data) { if (deployOption == "cloud") return { endpoint: "https://app.comfydeploy.com", apiUrl: "https://api.comfydeploy.com", apiKey: "", }; else return { endpoint: "http://localhost:3001", apiUrl: "http://localhost:3011", apiKey: "", }; } return { ...JSON.parse(data), environment: deployOption, }; } /** * Retrieves deployment data from local storage or defaults. * @param {{endpoint: string, apiKey: string, displayName: string, environment?: string}} [data] - The environment to get the data for. */ function saveData(data) { localStorage.setItem( "comfy_deploy_env_data_" + data.environment, JSON.stringify(data) ); } export class ConfigDialog extends ComfyDialog { container = null; poll = null; timeout = null; constructor() { super(); this.element.classList.add("comfy-normal-modal"); this.element.style.paddingBottom = "20px"; this.container = document.createElement("div"); this.element.querySelector(".comfy-modal-content").prepend(this.container); } createButtons() { return [ $el( "div", { type: "div", style: { display: "flex", gap: "6px", justifyContent: "flex-end", width: "100%", }, onclick: () => { this.save(); this.close(); }, }, [ $el("button", { type: "button", textContent: "Close", onclick: () => this.close(), }), $el("button", { type: "button", textContent: "Save", onclick: () => { this.save(); this.close(); }, }), ] ), ]; } close() { this.element.style.display = "none"; clearInterval(this.poll); clearTimeout(this.timeout); } async save(api_key, displayName) { const deployOption = this.container.querySelector("#deployOption").value; localStorage.setItem("comfy_deploy_env", deployOption); const endpoint = this.container.querySelector("#endpoint").value; const apiKey = api_key ?? this.container.querySelector("#apiKey").value; if (!displayName) { if (apiKey != getData().apiKey) { displayName = "Custom"; } else { displayName = getData().displayName; } } saveData({ endpoint, apiUrl: getData(deployOption).apiUrl, apiKey, displayName, environment: deployOption, }); // Refresh workflow list after saving configuration await refreshWorkflowListIfOpen(); } show() { this.container.style.color = "white"; const data = getData(); this.container.innerHTML = `

Comfy Deploy Config

API Key: User / Org ${ data.displayName ? `` : "" }
`; const button = this.container.querySelector("#loginButton"); button.onclick = () => { this.save(); const data = getData(); const uuid = crypto.randomUUID(); window.open(data.endpoint + "/auth/request/" + uuid, "_blank"); this.timeout = setTimeout(() => { clearInterval(this.poll); infoDialog.showError( "Timeout", "Wait too long for the response, please try re-login" ); }, 30000); // Stop polling after 30 seconds this.poll = setInterval(() => { fetch( `/comfyui-deploy/auth-response?request_id=${uuid}&api_url=${encodeURIComponent( data.apiUrl )}` ) .then((response) => response.json()) .then(async (json) => { if (json.api_key) { this.save(json.api_key, json.name); this.close(); this.container.querySelector("#apiKey").value = json.api_key; // infoDialog.show(); clearInterval(this.poll); clearTimeout(this.timeout); // Refresh dialog const a = await confirmDialog.confirm( "Authenticated", `
You will be able to upload workflow to
` ); configDialog.show(); } }) .catch((error) => { console.error("Error:", error); clearInterval(this.poll); clearTimeout(this.timeout); infoDialog.showMessage("Error", error); }); }, 2000); }; const apiKeyInput = this.container.querySelector("#apiKey"); apiKeyInput.addEventListener("paste", (e) => { e.stopPropagation(); }); const deployOption = this.container.querySelector("#deployOption"); const container = this.container; deployOption.addEventListener("change", function () { const selectedOption = this.value; const data = getData(selectedOption); localStorage.setItem("comfy_deploy_env", selectedOption); container.querySelector("#endpoint").value = data.endpoint; container.querySelector("#apiKey").value = data.apiKey; }); this.element.style.display = "flex"; this.element.style.zIndex = 1001; } } export const configDialog = new ConfigDialog(); const currentOrigin = window.location.origin; // const client = new ComfyDeploy({ // bearerAuth: getData().apiKey, // serverURL: `${currentOrigin}/comfydeploy/api/`, // }); const isComfyDeployDashboard = currentOrigin.includes("comfydeploy.com"); // Only register the sidebar tab if we're on localhost if (!isComfyDeployDashboard) { app.extensionManager.registerSidebarTab({ id: "search", icon: "pi pi-cloud-upload", title: "Deploy", tooltip: "Deploy and Configure", type: "custom", render: async (el) => { el.innerHTML = `

Comfy Deploy

Serverless Machine

${loadingIcon}
`; // Add deploy button const deployContainer = el.querySelector("#deploy-container"); const deployButton = document.createElement("button"); deployButton.id = "sidebar-deploy-button"; deployButton.style.display = "flex"; deployButton.style.alignItems = "center"; deployButton.style.justifyContent = "center"; deployButton.style.width = "100%"; deployButton.style.marginBottom = "10px"; deployButton.style.padding = "10px"; deployButton.style.fontSize = "16px"; deployButton.style.fontWeight = "bold"; deployButton.style.backgroundColor = "#4CAF50"; deployButton.style.color = "white"; deployButton.style.border = "none"; deployButton.style.borderRadius = "5px"; deployButton.style.cursor = "pointer"; deployButton.innerHTML = ``; deployButton.onclick = async () => { await deployWorkflow(); // Refresh the workflows list after deployment // refreshWorkflowsList(el); }; deployContainer.appendChild(deployButton); // Add config button const configContainer = el.querySelector("#config-container"); const configButton = document.createElement("button"); configButton.style.display = "flex"; configButton.style.alignItems = "center"; configButton.style.justifyContent = "center"; configButton.style.width = "100%"; configButton.style.padding = "8px"; configButton.style.fontSize = "14px"; configButton.style.backgroundColor = "#f0f0f0"; configButton.style.color = "#333"; configButton.style.border = "1px solid #ccc"; configButton.style.borderRadius = "5px"; configButton.style.cursor = "pointer"; configButton.innerHTML = `Configure`; configButton.onclick = () => { configDialog.show(); }; deployContainer.appendChild(configButton); // Fetch and display workflows const workflowsList = el.querySelector("#workflows-list"); const workflowsLoading = el.querySelector("#workflows-loading"); // Initialize machine manager await initializeMachineManager(el, getData); // Initialize workflows list and search const data = getData(); if (data.apiKey) { addWorkflowSearch(el, getData, getTimeAgo); await initializeWorkflowsList(el, getData, getTimeAgo); } }, }); } function getTimeAgo(date) { const seconds = Math.floor((new Date() - date) / 1000); let interval = seconds / 31536000; if (interval > 1) return Math.floor(interval) + " years ago"; interval = seconds / 2592000; if (interval > 1) return Math.floor(interval) + " months ago"; interval = seconds / 86400; if (interval > 1) return Math.floor(interval) + " days ago"; interval = seconds / 3600; if (interval > 1) return Math.floor(interval) + " hours ago"; interval = seconds / 60; if (interval > 1) return Math.floor(interval) + " minutes ago"; return Math.floor(seconds) + " seconds ago"; } // async function loadWorkflowApi(versionId) { // try { // const response = await client.comfyui.getWorkflowVersionVersionId({ // versionId: versionId, // }); // // Implement the logic to load the workflow API into the ComfyUI interface // console.log("Workflow API loaded:", response); // await window["app"].ui.settings.setSettingValueAsync( // "Comfy.Validation.Workflows", // true, // ); // app.loadGraphData(response.workflow); // // You might want to update the UI or trigger some action in ComfyUI here // } catch (error) { // console.error("Error loading workflow API:", error); // // Show an error message to the user // } // } const orginal_fetch_api = api.fetchApi; api.fetchApi = async (route, options) => { // console.log("Fetch API called with args:", route, options, ext.native_mode); if (route.startsWith("/prompt") && ext.native_mode) { const info = await getSelectedWorkflowInfo(); if (!info.workflow_id) { console.log("No workflow id found, fallback to original fetch"); return await orginal_fetch_api.call(api, route, options); } console.log("info", info); if (info) { const body = JSON.parse(options.body); const data = { client_id: body.client_id, workflow_api_json: body.prompt, workflow: body?.extra_data?.extra_pnginfo?.workflow, is_native_run: true, machine_id: info.machine_id, workflow_id: info.workflow_id, native_run_api_endpoint: info.native_run_api_endpoint, gpu_event_id: info.gpu_event_id, gpu: info.gpu, }; return await fetch("/comfyui-deploy/run", { method: "POST", headers: { Authorization: `Bearer ${info.cd_token}`, "Content-Type": "application/json", }, body: JSON.stringify(data), }); } } return await orginal_fetch_api.call(api, route, options); }; // Intercept window drag and drop events const originalDropHandler = document.ondrop; document.ondrop = async (e) => { console.log("Drop event intercepted:", e); // Prevent default browser behavior e.preventDefault(); // Handle files if present if (e.dataTransfer?.files?.length > 0) { const files = Array.from(e.dataTransfer.files); // Send file data to parent directly as JSON sendDirectEventToCD("file_drop", { files: files, x: e.clientX, y: e.clientY, timestamp: Date.now(), }); } // Call original handler if exists if (originalDropHandler) { originalDropHandler(e); } }; const originalDragEnterHandler = document.ondragenter; document.ondragenter = (e) => { // Prevent default to allow drop e.preventDefault(); // Send dragenter event to parent directly as JSON sendDirectEventToCD("file_dragenter", { x: e.clientX, y: e.clientY, timestamp: Date.now(), }); if (originalDragEnterHandler) { originalDragEnterHandler(e); } }; const originalDragLeaveHandler = document.ondragleave; document.ondragleave = (e) => { // Prevent default to allow drop e.preventDefault(); // Send dragleave event to parent directly as JSON sendDirectEventToCD("file_dragleave", { x: e.clientX, y: e.clientY, timestamp: Date.now(), }); if (originalDragLeaveHandler) { originalDragLeaveHandler(e); } }; const originalDragOverHandler = document.ondragover; document.ondragover = (e) => { // Prevent default to allow drop e.preventDefault(); // Send dragover event to parent directly as JSON sendDirectEventToCD("file_dragover", { x: e.clientX, y: e.clientY, timestamp: Date.now(), }); if (originalDragOverHandler) { originalDragOverHandler(e); } }; // Function to create a single button function createQueueButton(config) { const button = document.createElement("button"); button.id = `cd-button-${config.id}`; button.className = "p-button p-component p-button-icon-only p-button-secondary p-button-text"; button.innerHTML = `   `; button.onclick = () => { const eventData = typeof config.eventData === "function" ? config.eventData() : config.eventData || {}; sendEventToCD(config.event, eventData); }; button.setAttribute("data-pd-tooltip", config.tooltip); return button; } // Function to add buttons to queue group function addQueueButtons(buttonConfigs = DEFAULT_BUTTONS) { const queueButtonGroup = document.querySelector(".queue-button-group.flex"); if (!queueButtonGroup) return; // Remove any existing CD buttons const existingButtons = queueButtonGroup.querySelectorAll('[id^="cd-button-"]'); existingButtons.forEach((button) => button.remove()); // Add new buttons buttonConfigs.forEach((config) => { const button = createQueueButton(config); queueButtonGroup.appendChild(button); }); } // addMenuRightButtons([ // { // id: "cd-button-save-image", // icon: "pi-save", // label: "Snapshot", // tooltip: "Save the current image to your output directory.", // event: "save_image", // eventData: () => ({}), // }, // ]); // addMenuLeftButtons([ // { // id: "cd-button-back", // icon: ` // // `, // tooltip: "Go back to the previous page.", // event: "back", // eventData: () => ({}), // }, // ]); // addMenuButtons({ // containerSelector: "body > div.comfyui-body-top > div", // buttonConfigs: [ // { // id: "cd-button-workflow-1", // icon: ``, // label: "Workflow", // tooltip: "Go to Workflow 1", // event: "workflow_1", // // btnClasses: "", // eventData: () => ({}), // }, // { // id: "cd-button-workflow-3", // // icon: ``, // label: "v1", // tooltip: "Go to Workflow 1", // event: "workflow_1", // // btnClasses: "", // eventData: () => ({}), // }, // { // id: "cd-button-workflow-2", // icon: ``, // label: "Commit", // tooltip: "Commit the current workflow", // event: "commit", // style: { // backgroundColor: "oklch(.476 .114 61.907)", // }, // eventData: () => ({}), // }, // ], // buttonIdPrefix: "cd-button-workflow-", // insertBefore: // "body > div.comfyui-body-top > div > div.flex-grow.min-w-0.app-drag.h-full", // // containerStyle: { order: "3" } // }); // addMenuButtons({ // containerSelector: // "body > div.comfyui-body-top > div > div.flex-grow.min-w-0.app-drag.h-full", // clearContainer: true, // buttonConfigs: [], // buttonIdPrefix: "cd-button-p-", // containerStyle: { order: "-1" }, // }); // Function to add buttons to a menu container function addMenuButtons(options) { const { containerSelector, buttonConfigs, buttonIdPrefix = "cd-button-", containerClass = "comfyui-button-group", containerStyle = {}, clearContainer = false, insertBefore = null, // New option to specify selector for insertion point } = options; const menuContainer = document.querySelector(containerSelector); if (!menuContainer) return; // Remove any existing CD buttons const existingButtons = document.querySelectorAll( `[id^="${buttonIdPrefix}"]` ); existingButtons.forEach((button) => button.remove()); const container = document.createElement("div"); container.className = containerClass; // Apply container styles Object.assign(container.style, containerStyle); // Clear existing content if specified if (clearContainer) { menuContainer.innerHTML = ""; } // Create and add buttons buttonConfigs.forEach((config) => { const button = createMenuButton({ ...config, idPrefix: buttonIdPrefix, }); container.appendChild(button); }); // Insert before specified element if provided, otherwise append if (insertBefore) { const targetElement = menuContainer.querySelector(insertBefore); if (targetElement) { menuContainer.insertBefore(container, targetElement); } else { menuContainer.appendChild(container); } } else { menuContainer.appendChild(container); } } function createMenuButton(config) { const { id, icon, label, btnClasses = "", tooltip, event, eventData, idPrefix, style = {}, } = config; const button = document.createElement("button"); button.id = `${idPrefix}${id}`; button.className = `comfyui-button ${btnClasses}`; Object.assign(button.style, style); // Only add icon if provided const iconHtml = icon ? icon.startsWith("` : ""; button.innerHTML = ` ${iconHtml} ${label ? `${label}` : ""} `; button.onclick = () => { const data = typeof eventData === "function" ? eventData() : eventData || {}; sendEventToCD(event, data); }; if (tooltip) { button.setAttribute("data-pd-tooltip", tooltip); } return button; } // Refactored menu button functions function addMenuLeftButtons(buttonConfigs) { addMenuButtons({ containerSelector: "body > div.comfyui-body-top > div", buttonConfigs, buttonIdPrefix: "cd-button-left-", containerStyle: { order: "-1" }, }); } function addMenuRightButtons(buttonConfigs) { addMenuButtons({ containerSelector: ".comfyui-menu-right .flex", buttonConfigs, buttonIdPrefix: "cd-button-", containerStyle: {}, }); } // Function to refresh workflow list if sidebar is open async function refreshWorkflowListIfOpen() { try { const workflowsContainer = document.querySelector("#workflows-container"); const workflowsList = document.querySelector("#workflows-list"); // Only refresh if workflows container is visible and has been initialized if (!workflowsContainer || !workflowsList) { return; } // Import the functions we need const { initializeWorkflowsList } = await import("./workflow-list.js"); // Clear existing workflows and reset state safely workflowsList.innerHTML = ""; if (window.workflowsState) { window.workflowsState.initialized = false; window.workflowsState.workflows = []; window.workflowsState.offset = 0; window.workflowsState.hasMore = true; } // Reinitialize the workflow list await initializeWorkflowsList(document, getData, getTimeAgo); console.log("Workflow list refreshed successfully"); } catch (error) { console.error("Error refreshing workflow list:", error); } }