ui.js 8.38 KB
Newer Older
pythongosssss's avatar
pythongosssss committed
1
2
import { api } from "./api.js";

pythongosssss's avatar
pythongosssss committed
3
4
5
6
7
8
9
10
11
12
13
14
15
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.$;

pythongosssss's avatar
pythongosssss committed
16
17
18
19
20
			if (propsOrChildren.style) {
				Object.assign(element.style, propsOrChildren.style);
				delete propsOrChildren.style;
			}

pythongosssss's avatar
pythongosssss committed
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
			Object.assign(element, propsOrChildren);
			if (children) {
				element.append(...children);
			}

			if (parent) {
				parent.append(element);
			}

			if (cb) {
				cb(element);
			}
		}
	}
	return element;
}

pythongosssss's avatar
pythongosssss committed
38
39
class ComfyDialog {
	constructor() {
pythongosssss's avatar
pythongosssss committed
40
41
42
43
44
45
46
47
48
49
		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(),
				}),
			]),
		]);
pythongosssss's avatar
pythongosssss committed
50
51
52
53
54
55
56
57
58
59
60
61
	}

	close() {
		this.element.style.display = "none";
	}

	show(html) {
		this.textElement.innerHTML = html;
		this.element.style.display = "flex";
	}
}

pythongosssss's avatar
pythongosssss committed
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
class ComfySettingsDialog extends ComfyDialog {
	constructor() {
		super();
		this.element.classList.add("comfy-settings");
		this.settings = [];
	}

	addSetting({ id, name, type, defaultValue, onChange }) {
		if (!id) {
			throw new Error("Settings must have an ID");
		}
		if (this.settings.find((s) => s.id === id)) {
			throw new Error("Setting IDs must be unique");
		}

		const settingId = "Comfy.Settings." + id;
		const v = localStorage[settingId];
		let value = v == null ? defaultValue : JSON.parse(v);

		// Trigger initial setting of value
		if (onChange) {
			onChange(value, undefined);
		}

		this.settings.push({
			render: () => {
				const setter = (v) => {
					if (onChange) {
						onChange(v, value);
					}
					localStorage[settingId] = JSON.stringify(v);
					value = v;
				};

				if (typeof type === "function") {
					return type(name, setter);
				}

				switch (type) {
					case "boolean":
						return $el("div", [
							$el("label", { textContent: name || id }, [
								$el("input", {
									type: "checkbox",
									checked: !!value,
									oninput: (e) => {
										setter(e.target.checked);
									},
								}),
							]),
						]);
					default:
						console.warn("Unsupported setting type, defaulting to text");
						return $el("div", [
							$el("label", { textContent: name || id }, [
								$el("input", {
									value,
									oninput: (e) => {
										setter(e.target.value);
									},
								}),
							]),
						]);
				}
			},
		});
	}

	show() {
		super.show();
		this.textElement.replaceChildren(...this.settings.map((s) => s.render()));
	}
}

pythongosssss's avatar
pythongosssss committed
136
class ComfyList {
pythongosssss's avatar
pythongosssss committed
137
138
139
140
141
142
143
	#type;
	#text;

	constructor(text, type) {
		this.#text = text;
		this.#type = type || text.toLowerCase();
		this.element = $el("div.comfy-list");
pythongosssss's avatar
pythongosssss committed
144
145
146
147
148
149
150
151
		this.element.style.display = "none";
	}

	get visible() {
		return this.element.style.display !== "none";
	}

	async load() {
pythongosssss's avatar
pythongosssss committed
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
		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() }),
			])
		);
pythongosssss's avatar
pythongosssss committed
197
198
199
200
201
202
203
204
205
206
	}

	async update() {
		if (this.visible) {
			await this.load();
		}
	}

	async show() {
		this.element.style.display = "block";
pythongosssss's avatar
pythongosssss committed
207
208
		this.button.textContent = "Close";

pythongosssss's avatar
pythongosssss committed
209
210
211
212
213
		await this.load();
	}

	hide() {
		this.element.style.display = "none";
pythongosssss's avatar
pythongosssss committed
214
		this.button.textContent = "See " + this.#text;
pythongosssss's avatar
pythongosssss committed
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
	}

	toggle() {
		if (this.visible) {
			this.hide();
			return false;
		} else {
			this.show();
			return true;
		}
	}
}

export class ComfyUI {
	constructor(app) {
		this.app = app;
		this.dialog = new ComfyDialog();
pythongosssss's avatar
pythongosssss committed
232
		this.settings = new ComfySettingsDialog();
pythongosssss's avatar
pythongosssss committed
233

m957ymj75urz's avatar
m957ymj75urz committed
234
		this.batchCount = 1;
pythongosssss's avatar
pythongosssss committed
235
236
		this.queue = new ComfyList("Queue");
		this.history = new ComfyList("History");
pythongosssss's avatar
pythongosssss committed
237

pythongosssss's avatar
pythongosssss committed
238
239
240
241
		api.addEventListener("status", () => {
			this.queue.update();
			this.history.update();
		});
pythongosssss's avatar
pythongosssss committed
242

pythongosssss's avatar
pythongosssss committed
243
244
245
		const fileInput = $el("input", {
			type: "file",
			accept: ".json,image/png",
pythongosssss's avatar
pythongosssss committed
246
			style: { display: "none" },
pythongosssss's avatar
pythongosssss committed
247
248
249
250
			parent: document.body,
			onchange: () => {
				app.handleFile(fileInput.files[0]);
			},
pythongosssss's avatar
pythongosssss committed
251
		});
pythongosssss's avatar
pythongosssss committed
252

pythongosssss's avatar
pythongosssss committed
253
		this.menuContainer = $el("div.comfy-menu", { parent: document.body }, [
pythongosssss's avatar
pythongosssss committed
254
255
256
257
			$el("div", { style: { overflow: "hidden", position: "relative", width: "100%" } }, [
				$el("span", { $: (q) => (this.queueSize = q) }),
				$el("button.comfy-settings-btn", { textContent: "⚙️", onclick: () => this.settings.show() }),
			]),
m957ymj75urz's avatar
m957ymj75urz committed
258
259
260
			$el("button.comfy-queue-btn", { textContent: "Queue Prompt", onclick: () => app.queuePrompt(0, this.batchCount) }),
			$el("div", {}, [
				$el("label", { innerHTML: "Extra options"}, [
261
262
263
264
265
266
					$el("input", { type: "checkbox", 
						onchange: (i) => { 
							document.getElementById('extraOptions').style.visibility = i.srcElement.checked ? "visible" : "collapse";
							this.batchCount = i.srcElement.checked ? document.getElementById('batchCountInputRange').value : 1;
						}
					})
m957ymj75urz's avatar
m957ymj75urz committed
267
268
269
				])
			]),
			$el("div", { id: "extraOptions", style: { width: "100%", visibility: "collapse" }}, [
m957ymj75urz's avatar
m957ymj75urz committed
270
				$el("label", { innerHTML: "Batch count" }, [
m957ymj75urz's avatar
m957ymj75urz committed
271
272
273
274
275
276
277
278
279
280
281
282
					$el("input", { id: "batchCountInputNumber", type: "number", value: this.batchCount, min: "1", style: { width: "35%", "margin-left": "0.4em" }, 
						oninput: (i) => { 
							this.batchCount = i.target.value;
							document.getElementById('batchCountInputRange').value = this.batchCount;
						}
					}),
					$el("input", { id: "batchCountInputRange", type: "range", min: "1", max: "100", value: this.batchCount, 
						oninput: (i) => {
							this.batchCount = i.srcElement.value;
							document.getElementById('batchCountInputNumber').value = i.srcElement.value;
						}
					}),
m957ymj75urz's avatar
m957ymj75urz committed
283
284
				]),
			]),
pythongosssss's avatar
pythongosssss committed
285
			$el("div.comfy-menu-btns", [
m957ymj75urz's avatar
m957ymj75urz committed
286
				$el("button", { textContent: "Queue Front", onclick: () => app.queuePrompt(-1, this.batchCount) }),
pythongosssss's avatar
pythongosssss committed
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
				$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",
pythongosssss's avatar
pythongosssss committed
315
						style: { display: "none" },
pythongosssss's avatar
pythongosssss committed
316
317
318
319
320
321
322
323
324
						parent: document.body,
					});
					a.click();
					setTimeout(function () {
						a.remove();
						window.URL.revokeObjectURL(url);
					}, 0);
				},
			}),
pythongosssss's avatar
pythongosssss committed
325
			$el("button", { textContent: "Load", onclick: () => fileInput.click() }),
pythongosssss's avatar
pythongosssss committed
326
327
328
			$el("button", { textContent: "Clear", onclick: () => app.graph.clear() }),
			$el("button", { textContent: "Load Default", onclick: () => app.loadGraphData() }),
		]);
pythongosssss's avatar
pythongosssss committed
329
330
331
332
333
334
335
336

		this.setStatus({ exec_info: { queue_remaining: "X" } });
	}

	setStatus(status) {
		this.queueSize.textContent = "Queue size: " + (status ? status.exec_info.queue_remaining : "ERR");
	}
}