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";
const loadingIcon = ``;
function sendEventToCD(event, data) {
const message = {
type: event,
data: data,
};
window.parent.postMessage(JSON.stringify(message), "*");
}
/** @typedef {import('../../../web/types/comfy.js').ComfyExtension} ComfyExtension*/
/** @type {ComfyExtension} */
const ext = {
name: "BennyKok.ComfyUIDeploy",
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");
if (workspace_mode) {
document.querySelector(".comfy-menu").style.display = "none";
}
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 {
color = LGraphCanvas.node_colors.yellow.color;
bgcolor = LGraphCanvas.node_colors.yellow.bgcolor;
groupcolor = LGraphCanvas.node_colors.yellow.groupcolor;
constructor() {
if (!this.properties) {
this.properties = {};
this.properties.workflow_name = "";
this.properties.workflow_id = "";
this.properties.version = "";
}
ComfyWidgets.STRING(
this,
"workflow_name",
["", { default: this.properties.workflow_name, multiline: false }],
app,
);
ComfyWidgets.STRING(
this,
"workflow_id",
["", { default: this.properties.workflow_id, multiline: false }],
app,
);
ComfyWidgets.STRING(
this,
"version",
["", { default: this.properties.version, multiline: false }],
app,
);
// this.widgets.forEach((w) => {
// // w.computeSize = () => [200,10]
// w.computedHeight = 2;
// })
this.widgets_start_y = 10;
this.setSize(this.computeSize());
// const config = { };
// console.log(this);
this.serialize_widgets = true;
this.isVirtualNode = true;
}
}
// Load default visibility
LiteGraph.registerNodeType(
"ComfyDeploy",
Object.assign(ComfyDeploy, {
title_mode: LiteGraph.NORMAL_TITLE,
title: "Comfy Deploy",
collapsable: true,
}),
);
ComfyDeploy.category = "deploy";
},
async setup() {
// const graphCanvas = document.getElementById("graph-canvas");
window.addEventListener("message", async (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) {
app.loadGraphData(comfyUIWorkflow);
}
} else if (message.type === "deploy") {
// deployWorkflow();
const prompt = await app.graphToPrompt();
sendEventToCD("cd_plugin_onDeployChanges", prompt);
} else if (message.type === "queue_prompt") {
const prompt = await app.graphToPrompt();
sendEventToCD("cd_plugin_onQueuePrompt", prompt);
}
} catch (error) {
// console.error("Error processing message:", error);
}
// if (!event.data.flow || Object.entries(event.data.flow).length <= 0)
// return;
// updateBlendshapesPrompts(event.data.flow);
});
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) {
// }
});
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;
}
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),
`
${loadingIcon}
`,
// createDynamicUIHtml(deps),
);
if (!depsOk) return;
console.log(deps);
}
loadingDialog.showLoading("Deploying...");
title.innerText = "Deploying...";
title.style.color = "orange";
// console.log(prompt);
// TODO trim the ending / from endpoint is there is
if (endpoint.endsWith("/")) {
endpoint = endpoint.slice(0, -1);
}
// console.log(prompt.workflow);
const apiRoute = endpoint + "/api/workflow";
// const userId = apiKey
try {
const body = {
workflow_name,
workflow_id,
workflow: prompt.workflow,
workflow_api: prompt.output,
snapshot: snapshot,
dependencies: deps,
};
console.log(body);
let data = await fetch(apiRoute, {
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();
}
loadingDialog.close();
title.textContent = "Done";
title.style.color = "green";
deployMetaNode.widgets[1].value = data.workflow_id;
deployMetaNode.widgets[2].value = data.version;
graph.change();
infoDialog.show(
`Deployed successfully! -> View here
Workflow ID: ${data.workflow_id}
Workflow Name: ${workflow_name}
Workflow Version: ${data.version}
`,
);
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);
import { ComfyDialog, $el } from "../../scripts/ui.js";
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}
`);
}
}
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://www.comfydeploy.com",
apiKey: "",
};
else
return {
endpoint: "http://localhost:3000",
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);
}
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,
apiKey,
displayName,
environment: deployOption,
});
}
show() {
this.container.style.color = "white";
const data = getData();
this.container.innerHTML = `
`;
const button = this.container.querySelector("#loginButton");
button.onclick = () => {
this.save();
const data = getData();
const uuid =
Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
window.open(data.endpoint + "/auth-request/" + uuid, "_blank");
this.timeout = setTimeout(() => {
clearInterval(poll);
infoDialog.showMessage(
"Timeout",
"Wait too long for the response, please try re-login",
);
}, 30000); // Stop polling after 30 seconds
this.poll = setInterval(() => {
fetch(data.endpoint + "/api/auth-response/" + uuid)
.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();