diff --git a/server.py b/server.py index 34566376..cfc103cb 100644 --- a/server.py +++ b/server.py @@ -144,7 +144,12 @@ class PromptServer(): self.prompt_queue.delete_queue_item(delete_func) return web.Response(status=200) - + + @routes.post("/interrupt") + async def post_interrupt(request): + nodes.interrupt_processing() + return web.Response(status=200) + @routes.post("/history") async def post_history(request): json_data = await request.json() diff --git a/web/index.html b/web/index.html index af0646b9..da611ddc 100644 --- a/web/index.html +++ b/web/index.html @@ -20,30 +20,6 @@ let progress = null; let clientId = null; - function clearGraph() { - graph.clear(); - } - - function loadTxt2Img() { - loadGraphData(graph, default_graph); - } - - function saveGraph() { - var json = JSON.stringify(graph.serialize()); // convert the data to a JSON string - var blob = new Blob([json], { type: "application/json" }); - var url = URL.createObjectURL(blob); - var a = document.createElement("a"); - a.style = "display: none"; - a.href = url; - a.download = "workflow.json"; - document.body.appendChild(a); - a.click(); - setTimeout(function () { - document.body.removeChild(a); - window.URL.revokeObjectURL(url); - }, 0); - } - var input = document.createElement("input"); input.setAttribute("type", "file"); input.setAttribute("accept", ".json,image/png"); @@ -58,157 +34,6 @@ function loadGraph() { input.click(); } - - document.addEventListener("paste", (e) => { - let data = (e.clipboardData || window.clipboardData).getData("text/plain"); - console.log(data); - - try { - data = data.slice(data.indexOf("{")); - j = JSON.parse(data); - } catch (err) { - data = data.slice(data.indexOf("workflow\n")); - data = data.slice(data.indexOf("{")); - j = JSON.parse(data); - } - - if (Object.hasOwn(j, "version") && Object.hasOwn(j, "nodes") && Object.hasOwn(j, "extra")) { - loadGraphData(graph, j); - } - }); - - function deleteQueueElement(type, delete_id, then) { - fetch("/" + type, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ delete: [delete_id] }), - }) - .then((data) => { - console.log(data); - then(); - }) - .catch((error) => console.error(error)); - } - - function loadQueue() { - loadItems("queue"); - } - function loadHistory() { - loadItems("history"); - } - function loadItems(type) { - fetch("/" + type) - .then((response) => response.json()) - .then((data) => { - var queue_div = document.getElementById(type + "button-content"); - queue_div.style.display = "block"; - var see_queue_button = document.getElementById("see" + type + "button"); - let old_w = see_queue_button.style.width; - see_queue_button.innerHTML = "Close"; - - let runningcontents; - if (type === "queue") { - runningcontents = document.getElementById("runningcontents"); - runningcontents.innerHTML = ""; - } - let queuecontents = document.getElementById(type + "contents"); - queuecontents.innerHTML = ""; - function append_to_list(list_element, append_to_element, append_delete, state) { - let number = list_element[0]; - let id = list_element[1]; - let prompt = list_element[2]; - let workflow = list_element[3].extra_pnginfo.workflow; - let a = document.createElement("a"); - a.innerHTML = number + ": "; - append_to_element.appendChild(a); - let button = document.createElement("button"); - button.innerHTML = "Load"; - button.style.fontSize = "10px"; - button.workflow = workflow; - button.onclick = function (event) { - loadGraphData(graph, event.target.workflow); - if (state) { - nodeOutputs = state; - } - }; - - append_to_element.appendChild(button); - if (append_delete) { - let button = document.createElement("button"); - button.innerHTML = "Delete"; - button.style.fontSize = "10px"; - button.delete_id = id; - button.onclick = function (event) { - deleteQueueElement(type, event.target.delete_id, () => loadItems(type)); - }; - append_to_element.appendChild(button); - } - append_to_element.appendChild(document.createElement("br")); - } - - if (runningcontents) { - for (let x in data.queue_running) { - append_to_list(data.queue_running[x], runningcontents, false); - } - } - - let items; - if (type === "queue") { - items = data.queue_pending; - } else { - items = Object.values(data); - } - items.sort((a, b) => a[0] - b[0]); - for (let i of items) { - append_to_list(type === "queue" ? i : i.prompt, queuecontents, true, i.outputs); - } - }) - .catch((response) => { - console.log(response); - }); - } - - function seeItems(type) { - var queue_div = document.getElementById(type + "button-content"); - if (queue_div.style.display == "block") { - closeItems(type); - } else { - loadItems(type); - } - } - - function seeQueue() { - closeItems("history"); - seeItems("queue"); - } - - function seeHistory() { - closeItems("queue"); - seeItems("history"); - } - - function closeItems(type) { - var queue_div = document.getElementById(type + "button-content"); - queue_div.style.display = "none"; - var see_queue_button = document.getElementById("see" + type + "button"); - see_queue_button.innerHTML = "See " + type[0].toUpperCase() + type.substr(1); - } - - function clearItems(type) { - fetch("/" + type, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ clear: true }), - }) - .then((data) => { - loadItems(type); - }) - .catch((error) => console.error(error)); - } diff --git a/web/scripts/api.js b/web/scripts/api.js index 322cd95f..b08f95d6 100644 --- a/web/scripts/api.js +++ b/web/scripts/api.js @@ -1,6 +1,6 @@ class ComfyApi extends EventTarget { constructor() { - super(); + super(); } #pollQueue() { @@ -74,9 +74,9 @@ class ComfyApi extends EventTarget { }); } - init() { - this.#createSocket(); - } + init() { + this.#createSocket(); + } async getNodeDefs() { const resp = await fetch("object_info", { cache: "no-store" }); @@ -110,6 +110,64 @@ class ComfyApi extends EventTarget { }; } } + + async getItems(type) { + if (type === "queue") { + return this.getQueue(); + } + return this.getHistory(); + } + + async getQueue() { + try { + const res = await fetch("/queue"); + const data = await res.json(); + return { + // Running action uses a different endpoint for cancelling + Running: data.queue_running.map((prompt) => ({ prompt, remove: { name: "Cancel", cb: () => api.interrupt() } })), + Pending: data.queue_pending.map((prompt) => ({ prompt })), + }; + } catch (error) { + console.error(error); + return { Running: [], Pending: [] }; + } + } + + async getHistory() { + try { + const res = await fetch("/history"); + return { History: Object.values(await res.json()) }; + } catch (error) { + console.error(error); + return { History: [] }; + } + } + + async #postItem(type, body) { + try { + await fetch("/" + type, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: body ? JSON.stringify(body) : undefined, + }); + } catch (error) { + console.error(error); + } + } + + async deleteItem(type, id) { + await this.#postItem(type, { delete: [id] }); + } + + async clearItems(type) { + await this.#postItem(type, { clear: true }); + } + + async interrupt() { + await this.#postItem("interrupt", null); + } } export const api = new ComfyApi(); diff --git a/web/scripts/app.js b/web/scripts/app.js index d3904a95..4c0eb510 100644 --- a/web/scripts/app.js +++ b/web/scripts/app.js @@ -313,8 +313,27 @@ class ComfyApp { }; reader.readAsText(file); } + }); + } + + #addPasteHandler() { + document.addEventListener("paste", (e) => { + let data = (e.clipboardData || window.clipboardData).getData("text/plain"); + let workflow; + try { + data = data.slice(data.indexOf("{")); + workflow = JSON.parse(data); + } catch (err) { + try { + data = data.slice(data.indexOf("workflow\n")); + data = data.slice(data.indexOf("{")); + workflow = JSON.parse(data); + } catch (error) {} + } - prompt_file_load(file); + if (workflow && workflow.version && workflow.nodes && workflow.extra) { + this.loadGraphData(workflow); + } }); } @@ -367,13 +386,17 @@ class ComfyApp { } #addApiUpdateHandlers() { - api.addEventListener("status", (status) => { - console.log(status); + api.addEventListener("status", ({ detail }) => { + this.ui.setStatus(detail); }); - api.addEventListener("reconnecting", () => {}); + api.addEventListener("reconnecting", () => { + this.ui.dialog.show("Reconnecting..."); + }); - api.addEventListener("reconnected", () => {}); + api.addEventListener("reconnected", () => { + this.ui.dialog.close(); + }); api.addEventListener("progress", ({ detail }) => { this.progress = detail; @@ -386,7 +409,9 @@ class ComfyApp { this.graph.setDirtyCanvas(true, false); }); - api.addEventListener("executed", (e) => {}); + api.addEventListener("executed", ({ detail }) => { + this.nodeOutputs[detail.node] = detail.output; + }); api.init(); } @@ -431,7 +456,7 @@ class ComfyApp { // We failed to restore a workflow so load the default if (!restored) { - this.loadGraphData(defaultGraph); + this.loadGraphData(); } // Save current workflow automatically @@ -440,6 +465,8 @@ class ComfyApp { this.#addDrawNodeProgressHandler(); this.#addApiUpdateHandlers(); this.#addDropHandler(); + this.#addPasteHandler(); + await this.#invokeExtensionsAsync("setup"); } @@ -511,6 +538,9 @@ class ComfyApp { * @param {*} graphData A serialized graph object */ loadGraphData(graphData) { + if (!graphData) { + graphData = defaultGraph; + } this.graph.configure(graphData); for (const node of this.graph._nodes) { @@ -526,7 +556,7 @@ class ComfyApp { if (node.type == "KSampler" || node.type == "KSamplerAdvanced") { if (widget.name == "sampler_name") { if (widget.value.startsWith("sample_")) { - wid.value = widget.value.slice(7); + widget.value = widget.value.slice(7); } } } diff --git a/web/scripts/extensions.js b/web/scripts/extensions.js new file mode 100644 index 00000000..fb106ae0 --- /dev/null +++ b/web/scripts/extensions.js @@ -0,0 +1,9 @@ +export class ComfyExtension { + init(app) {} + setup(app) {} + loadedGraphNode(node, app) {} + addCustomNodeDefs(defs, app) {} + getCustomWidgets(app) {} + beforeRegisterNode(nodeType, nodeData, app) {} + registerCustomNodes(app) {} +} \ No newline at end of file diff --git a/web/scripts/ui.js b/web/scripts/ui.js index b7f24c4f..42da6772 100644 --- a/web/scripts/ui.js +++ b/web/scripts/ui.js @@ -1,23 +1,47 @@ import { api } from "./api.js"; +function $el(tag, propsOrChildren, children) { + const split = tag.split("."); + const element = document.createElement(split.shift()); + element.classList.add(...split); + if (propsOrChildren) { + if (Array.isArray(propsOrChildren)) { + element.append(...propsOrChildren); + } else { + const parent = propsOrChildren.parent; + delete propsOrChildren.parent; + const cb = propsOrChildren.$; + delete propsOrChildren.$; + + Object.assign(element, propsOrChildren); + if (children) { + element.append(...children); + } + + if (parent) { + parent.append(element); + } + + if (cb) { + cb(element); + } + } + } + return element; +} + class ComfyDialog { constructor() { - this.element = document.createElement("div"); - this.element.classList.add("comfy-modal"); - - const content = document.createElement("div"); - content.classList.add("comfy-modal-content"); - this.textElement = document.createElement("p"); - content.append(this.textElement); - - const closeBtn = document.createElement("button"); - closeBtn.type = "button"; - closeBtn.textContent = "CLOSE"; - content.append(closeBtn); - closeBtn.onclick = () => this.close(); - - this.element.append(content); - document.body.append(this.element); + this.element = $el("div.comfy-modal", { parent: document.body }, [ + $el("div.comfy-modal-content", [ + $el("p", { $: (p) => (this.textElement = p) }), + $el("button", { + type: "button", + textContent: "CLOSE", + onclick: () => this.close(), + }), + ]), + ]); } close() { @@ -31,10 +55,14 @@ class ComfyDialog { } class ComfyList { - constructor() { - this.element = document.createElement("div"); + #type; + #text; + + constructor(text, type) { + this.#text = text; + this.#type = type || text.toLowerCase(); + this.element = $el("div.comfy-list"); this.element.style.display = "none"; - this.element.textContent = "hello"; } get visible() { @@ -42,7 +70,51 @@ class ComfyList { } async load() { - // const queue = await api.getQueue(); + const items = await api.getItems(this.#type); + this.element.replaceChildren( + ...Object.keys(items).flatMap((section) => [ + $el("h4", { + textContent: section, + }), + $el("div.comfy-list-items", [ + ...items[section].map((item) => { + // Allow items to specify a custom remove action (e.g. for interrupt current prompt) + const removeAction = item.remove || { + name: "Delete", + cb: () => api.deleteItem(this.#type, item.prompt[1]), + }; + return $el("div", { textContent: item.prompt[0] + ": " }, [ + $el("button", { + textContent: "Load", + onclick: () => { + if (item.outputs) { + app.nodeOutputs = item.outputs; + } + app.loadGraphData(item.prompt[3].extra_pnginfo.workflow); + }, + }), + $el("button", { + textContent: removeAction.name, + onclick: async () => { + await removeAction.cb(); + await this.update(); + }, + }), + ]); + }), + ]), + ]), + $el("div.comfy-list-actions", [ + $el("button", { + textContent: "Clear " + this.#text, + onclick: async () => { + await api.clearItems(this.#type); + await this.load(); + }, + }), + $el("button", { textContent: "Refresh", onclick: () => this.load() }), + ]) + ); } async update() { @@ -53,11 +125,14 @@ class ComfyList { async show() { this.element.style.display = "block"; + this.button.textContent = "Close"; + await this.load(); } hide() { this.element.style.display = "none"; + this.button.textContent = "See " + this.#text; } toggle() { @@ -75,77 +150,78 @@ export class ComfyUI { constructor(app) { this.app = app; this.dialog = new ComfyDialog(); - this.queue = new ComfyList(); - this.history = new ComfyList(); - - this.menuContainer = document.createElement("div"); - this.menuContainer.classList.add("comfy-menu"); - - this.queueSize = document.createElement("span"); - this.menuContainer.append(this.queueSize); - - this.addAction("Queue Prompt", () => { - app.queuePrompt(0); - }, "queue"); - this.btnContainer = document.createElement("div"); - this.btnContainer.classList.add("comfy-menu-btns"); - this.menuContainer.append(this.btnContainer); + this.queue = new ComfyList("Queue"); + this.history = new ComfyList("History"); - this.addAction( - "Queue Front", - () => { - app.queuePrompt(-1); - }, - "sm" - ); + api.addEventListener("status", () => { + this.queue.update(); + this.history.update(); + }); - this.addAction( - "See Queue", - (btn) => { - btn.textContent = this.queue.toggle() ? "Close" : "See Queue"; - }, - "sm" - ); + var input = document.createElement("input"); + input.setAttribute("type", "file"); + input.setAttribute("accept", ".json,image/png"); + input.style.display = "none"; + document.body.appendChild(input); - this.addAction( - "See History", - (btn) => { - btn.textContent = this.history.toggle() ? "Close" : "See History"; - }, - "sm" - ); + input.addEventListener("change", function () { + var file = input.files[0]; + prompt_file_load(file); + }); - this.menuContainer.append(this.queue.element); - this.menuContainer.append(this.history.element); - this.addAction("Save", () => { - app.queuePrompt(-1); - }); - this.addAction("Load", () => { - app.queuePrompt(-1); - }); - this.addAction("Clear", () => { - app.queuePrompt(-1); - }); - this.addAction("Load Default", () => { - app.queuePrompt(-1); - }); + this.menuContainer = $el("div.comfy-menu", { parent: document.body }, [ + $el("span", { $: (q) => (this.queueSize = q) }), + $el("button.comfy-queue-btn", { textContent: "Queue Prompt", onclick: () => app.queuePrompt(0) }), + $el("div.comfy-menu-btns", [ + $el("button", { textContent: "Queue Front", onclick: () => app.queuePrompt(-1) }), + $el("button", { + $: (b) => (this.queue.button = b), + textContent: "View Queue", + onclick: () => { + this.history.hide(); + this.queue.toggle(); + }, + }), + $el("button", { + $: (b) => (this.history.button = b), + textContent: "View History", + onclick: () => { + this.queue.hide(); + this.history.toggle(); + }, + }), + ]), + this.queue.element, + this.history.element, + $el("button", { + textContent: "Save", + onclick: () => { + const json = JSON.stringify(app.graph.serialize()); // convert the data to a JSON string + const blob = new Blob([json], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const a = $el("a", { + href: url, + download: "workflow.json", + style: "display: none", + parent: document.body, + }); + a.click(); + setTimeout(function () { + a.remove(); + window.URL.revokeObjectURL(url); + }, 0); + }, + }), + $el("button", { textContent: "Load", onclick: () => {} }), + $el("button", { textContent: "Clear", onclick: () => app.graph.clear() }), + $el("button", { textContent: "Load Default", onclick: () => app.loadGraphData() }), + ]); - document.body.append(this.menuContainer); this.setStatus({ exec_info: { queue_remaining: "X" } }); } - addAction(text, cb, cls) { - const btn = document.createElement("button"); - btn.classList.add("comfy-menu-btn-" + (cls || "lg")); - btn.textContent = text; - btn.onclick = () => { - cb(btn); - }; - (cls === "sm" ? this.btnContainer : this.menuContainer).append(btn); - } - setStatus(status) { this.queueSize.textContent = "Queue size: " + (status ? status.exec_info.queue_remaining : "ERR"); } diff --git a/web/style.css b/web/style.css index bb28ad9f..c262b2f4 100644 --- a/web/style.css +++ b/web/style.css @@ -34,6 +34,8 @@ body { max-height: 80vh; transform: translate(-50%, -50%); overflow: hidden; + min-width: 60%; + justify-content: center; } .comfy-modal-content { @@ -79,21 +81,56 @@ body { align-items: center; } +.comfy-menu button { + font-size: 20px; +} + .comfy-menu-btns { margin-bottom: 10px; + width: 100%; } -.comfy-menu-btn-sm { +.comfy-menu-btns button { font-size: 10px; width: 50%; } -.comfy-menu-btn-lg, .comfy-menu-btn-queue { - font-size: 20px; +.comfy-queue-btn { + width: 100%; } -.comfy-menu-btn-queue { - width: 100%; +.comfy-list { + background-color: rgb(225, 225, 225); + margin-bottom: 10px; +} + +.comfy-list-items { + overflow-y: scroll; + max-height: 100px; + background-color: #d0d0d0; + padding: 5px; +} + +.comfy-list h4 { + min-width: 160px; + margin: 0; + padding: 3px; + font-weight: normal; +} + +.comfy-list-items button { + font-size: 10px; +} + +.comfy-list-actions { + margin: 5px; + display: flex; + gap: 5px; + justify-content: center; +} + +.comfy-list-actions button { + font-size: 12px; } @media (prefers-color-scheme: dark) { @@ -107,7 +144,7 @@ body { } @media only screen and (max-height: 850px) { - #menu { + .comfy-menu { margin-top: -70px; } }