import { app } from "./app.js"; import { api } from "./api.js"; import { ComfyWidgets, LGraphNode } from "./widgets.js"; import { generateDependencyGraph } from "https://esm.sh/comfyui-json@0.1.25"; import { ComfyDeploy } from "https://esm.sh/comfydeploy@0.0.19-beta.30"; const loadingIcon = ``; function sendEventToCD(event, data) { const message = { type: event, data: data, }; window.parent.postMessage(JSON.stringify(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; } /** @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, ); }); } }, 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) // Ensure all necessary data is restored if (o.properties) { 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"; }, 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", false, ); } catch (error) { console.warning( "Error setting validation to false, is fine to ignore this", error, ); } console.log("loadGraphData"); app.loadGraphData(comfyUIWorkflow); } } 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 === "get_prompt") { const prompt = await app.graphToPrompt(); sendEventToCD("cd_plugin_onGetPrompt", prompt); } else if (message.type === "event") { dispatchAPIEventData(message.data); } 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', 'right') } 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, displayName } = getData(); if (!endpoint || !apiKey || apiKey === "" || endpoint === "") { 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 includeDeps = document.getElementById("include-deps").checked; const reuseHash = document.getElementById("reuse-hash").checked; if (endpoint.endsWith("/")) { endpoint = endpoint.slice(0, -1); } loadingDialog.showLoading("Generating snapshot"); const snapshot = await fetch("/snapshot/get_current").then((x) => x.json()); // 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"); const prompt = await app.graphToPrompt(); let deps = undefined; if (includeDeps) { loadingDialog.showLoading("Fetching existing version"); const existing_workflow = await fetch( endpoint + "/api/workflow/" + workflow_id, { method: "GET", headers: { "Content-Type": "application/json", Authorization: "Bearer " + apiKey, }, }, ) .then((x) => x.json()) .catch(() => { return {}; }); loadingDialog.close(); loadingDialog.showLoading("Generating dependency graph"); deps = await generateDependencyGraph({ workflow_api: prompt.output, snapshot: snapshot, computeFileHash: async (file) => { console.log(existing_workflow?.dependencies?.models); // Match previous hash for models if (reuseHash && existing_workflow?.dependencies?.models) { const previousModelHash = Object.entries( existing_workflow?.dependencies?.models, ).flatMap(([key, value]) => { return Object.values(value).map((x) => ({ ...x, name: "models/" + key + "/" + x.name, })); }); console.log(previousModelHash); const match = previousModelHash.find((x) => { console.log(file, x.name); return file == x.name; }); console.log(match); if (match && match.hash) { console.log("cached hash used"); return match.hash; } } console.log(file); loadingDialog.showLoading("Generating hash", file); const hash = await fetch( `/comfyui-deploy/get-file-hash?file_path=${encodeURIComponent(file)}`, ).then((x) => x.json()); loadingDialog.showLoading("Generating hash", file); console.log(hash); return hash.file_hash; }, // handleFileUpload: async (file, hash, prevhash) => { // console.log("Uploading ", file); // loadingDialog.showLoading("Uploading file", file); // try { // const { download_url } = await fetch(`/comfyui-deploy/upload-file`, { // method: "POST", // body: JSON.stringify({ // file_path: file, // token: apiKey, // url: endpoint + "/api/upload-url", // }), // }) // .then((x) => x.json()) // .catch(() => { // loadingDialog.close(); // confirmDialog.confirm("Error", "Unable to upload file " + file); // }); // loadingDialog.showLoading("Uploaded file", file); // console.log(download_url); // return download_url; // } catch (error) { // return undefined; // } // }, existingDependencies: existing_workflow.dependencies, }); // Need to find a way to include this if this is not included in comfyui-json level if ( !deps.custom_nodes["https://github.com/BennyKok/comfyui-deploy"] && !deps.custom_nodes["https://github.com/BennyKok/comfyui-deploy.git"] ) deps.custom_nodes["https://github.com/BennyKok/comfyui-deploy"] = { url: "https://github.com/BennyKok/comfyui-deploy", install_type: "git-clone", hash: snapshot?.git_custom_nodes?.[ "https://github.com/BennyKok/comfyui-deploy" ]?.hash ?? "HEAD", name: "ComfyUI Deploy", }; loadingDialog.close(); const depsOk = await confirmDialog.confirm( "Check dependencies", // JSON.stringify(deps, null, 2), `
You will need to create a cloud machine with the following configuration on ComfyDeploy
  1. Review the dependencies listed in the graph below
  2. Create a new cloud machine with the required configuration
  3. Install missing models and check missing files
  4. Deploy your workflow to the newly created machine
${loadingIcon}