const { app } = window.comfyAPI.app; const { api } = window.comfyAPI.api; function findWidget(node, name) { return (node.widgets || []).find((widget) => widget.name === name); } function findInput(node, name) { return (node.inputs || []).find((input) => input.name === name); } function getOriginNodeForInput(node, inputName) { const input = findInput(node, inputName); if (!input || input.link == null) return null; const link = app.graph.links[input.link]; if (!link) return null; return app.graph.getNodeById(link.origin_id); } function coercePreviewUrl(value) { if (!value) return ""; if (typeof value === "string") return value; if (typeof value === "object") { if (typeof value.src === "string") return value.src; if (typeof value.url === "string") return value.url; } return ""; } function resolveFileWidgetUrlFromOrigin(originNode) { if (!originNode) return ""; const widgets = originNode.widgets || []; const fileWidget = widgets.find((widget) => ["image", "filename", "file"].includes(widget.name)); const subfolderWidget = widgets.find((widget) => widget.name === "subfolder"); const filename = typeof fileWidget?.value === "string" ? fileWidget.value : ""; const subfolder = typeof subfolderWidget?.value === "string" ? subfolderWidget.value : ""; if (!filename) return ""; return api.apiURL( `/view?filename=${encodeURIComponent(filename)}&type=input&subfolder=${encodeURIComponent(subfolder)}` ); } function resolvePreviewUrlFromOrigin(originNode) { if (!originNode) return ""; const preview = Array.isArray(originNode.imgs) ? coercePreviewUrl(originNode.imgs[0]) : ""; return preview || ""; } function resolveCandidateImageUrls(originNode) { const urls = []; const fileUrl = resolveFileWidgetUrlFromOrigin(originNode); const previewUrl = resolvePreviewUrlFromOrigin(originNode); if (fileUrl) urls.push(fileUrl); if (previewUrl && previewUrl !== fileUrl) urls.push(previewUrl); return urls; } function setWidgetValue(node, widget, value) { if (!widget) return; const previous = widget.value; widget.value = value; if (widget.element) { if (widget.element.type === "checkbox") { widget.element.checked = !!value; } else { widget.element.value = value; } } if (widget.callback) { try { widget.callback(value); } catch (error) {} } if (node.onWidgetChanged) { try { node.onWidgetChanged(widget.name, value, previous, widget); } catch (error) {} } if (node.properties) { node.properties[widget.name] = value; } } function syncFormFromWidgets(node) { const description = findWidget(node, "description")?.value || ""; const alias = findWidget(node, "alias")?.value || ""; if (node._msrAliasInput && node._msrAliasInput.value !== alias) { node._msrAliasInput.value = alias; } if (node._msrDescriptionInput && node._msrDescriptionInput.value !== description) { node._msrDescriptionInput.value = description; } if (node._msrMeta) { node._msrMeta.innerHTML = [ alias ? `Alias: @${String(alias).replace(/^@/, "")}` : "Alias: none", `
Description is stored in the field below and appended to the director prompt as a tagged character reference.
`, ].join(""); } } function refreshCharacterPreview(node) { if (!node._msrPreviewRow) return; const inputNames = ["image_1", "image_2"]; const previewUrls = inputNames .map((inputName) => { const originNode = getOriginNodeForInput(node, inputName); const candidates = resolveCandidateImageUrls(originNode); return candidates[0] || ""; }) .filter(Boolean); const previewRow = node._msrPreviewRow; previewRow.innerHTML = ""; if (previewUrls.length) { previewUrls.forEach((previewUrl) => { const image = document.createElement("img"); image.src = previewUrl; Object.assign(image.style, { flex: "1", minWidth: "0", height: "74px", objectFit: "cover", borderRadius: "6px", border: "1px solid #444", background: "#111", }); previewRow.appendChild(image); }); } else { const placeholder = document.createElement("div"); placeholder.textContent = "Connect up to 2 image nodes"; Object.assign(placeholder.style, { width: "100%", height: "74px", display: "flex", alignItems: "center", justifyContent: "center", color: "#777", fontSize: "11px", borderRadius: "6px", border: "1px dashed #444", background: "#181818", }); previewRow.appendChild(placeholder); } syncFormFromWidgets(node); } function buildCharacterUi(node) { const container = document.createElement("div"); Object.assign(container.style, { boxSizing: "border-box", width: "100%", padding: "6px 2px 2px 2px", }); const previewRow = document.createElement("div"); Object.assign(previewRow.style, { display: "flex", gap: "6px", minHeight: "74px", }); const meta = document.createElement("div"); Object.assign(meta.style, { marginTop: "8px", fontSize: "11px", color: "#aaa", lineHeight: "1.4", }); const form = document.createElement("div"); Object.assign(form.style, { marginTop: "10px", display: "flex", flexDirection: "column", gap: "6px", }); const aliasLabel = document.createElement("label"); aliasLabel.textContent = "Alias"; Object.assign(aliasLabel.style, { fontSize: "11px", color: "#bbb", }); const aliasInput = document.createElement("input"); aliasInput.type = "text"; aliasInput.placeholder = "optional alias, e.g. bob"; Object.assign(aliasInput.style, { width: "100%", boxSizing: "border-box", background: "#1c1c1c", color: "#eee", border: "1px solid #444", borderRadius: "6px", padding: "6px 8px", fontSize: "12px", }); aliasInput.addEventListener("input", () => { setWidgetValue(node, findWidget(node, "alias"), aliasInput.value); syncFormFromWidgets(node); }); const descLabel = document.createElement("label"); descLabel.textContent = "Description"; Object.assign(descLabel.style, { fontSize: "11px", color: "#bbb", }); const descInput = document.createElement("textarea"); descInput.placeholder = "Character description..."; Object.assign(descInput.style, { width: "100%", minHeight: "86px", boxSizing: "border-box", resize: "vertical", background: "#1c1c1c", color: "#eee", border: "1px solid #444", borderRadius: "6px", padding: "8px", fontSize: "12px", lineHeight: "1.35", }); descInput.addEventListener("input", () => { setWidgetValue(node, findWidget(node, "description"), descInput.value); syncFormFromWidgets(node); }); form.appendChild(aliasLabel); form.appendChild(aliasInput); form.appendChild(descLabel); form.appendChild(descInput); container.appendChild(previewRow); container.appendChild(meta); container.appendChild(form); node._msrPreviewContainer = container; node._msrPreviewRow = previewRow; node._msrMeta = meta; node._msrAliasInput = aliasInput; node._msrDescriptionInput = descInput; return container; } function hideNodeWidget(widget) { if (!widget) return; widget.hidden = true; if (!widget.options) widget.options = {}; widget.options.hidden = true; if (widget.element) { widget.element.style.display = "none"; } } app.registerExtension({ name: "Comfy.MSRCharacterCS", async beforeRegisterNodeDef(nodeType, nodeData) { if (nodeData.name === "MSRCharacterCS") { const originalOnNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function () { if (originalOnNodeCreated) originalOnNodeCreated.apply(this, arguments); if (!this._msrPreviewWidget) { const previewContainer = buildCharacterUi(this); this._msrPreviewWidget = this.addDOMWidget("msr_character_ui", "msr_character_ui", previewContainer, { getValue: () => "", setValue: () => {}, }); this._msrPreviewWidget.computeSize = () => [Math.max(10, (this.size?.[0] || 320) - 30), 260]; } const originalConnectionsChange = this.onConnectionsChange; this.onConnectionsChange = function () { const result = originalConnectionsChange?.apply(this, arguments); refreshCharacterPreview(this); return result; }; const originalConfigure = this.onConfigure; this.onConfigure = function () { const result = originalConfigure?.apply(this, arguments); setTimeout(() => { syncFormFromWidgets(this); refreshCharacterPreview(this); }, 0); return result; }; ["alias", "description"].forEach((widgetName) => { hideNodeWidget(findWidget(this, widgetName)); }); this.size[0] = Math.max(this.size[0] || 0, 330); syncFormFromWidgets(this); refreshCharacterPreview(this); }; } if (nodeData.name === "MSRCharacterSetCS") { const originalOnNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function () { if (originalOnNodeCreated) originalOnNodeCreated.apply(this, arguments); this.size[0] = Math.max(this.size[0] || 0, 260); }; } }, });