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);
};
}
},
});