Files
Danchi93-ComfyUI-Workflow-T…/prompt_segments.js
T
2026-07-04 13:36:57 +08:00

240 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { app } from "../../scripts/app.js";
// 语言配置:将下面一行的 "en" 改成 "zh" 即可切换中文
const LANG = "en";
const I18N = {
en: { label_ph: "Label...", add: "+ Add Segment", default_label: "Quality Tags",
seg_ph: (i) => `Enter segment ${i} prompt...`, order_tip: "Change order (move this segment)" },
zh: { label_ph: "备注标签...", add: "+ 添加段落", default_label: "质量词",
seg_ph: (i) => `输入第 ${i} 段提示词...`, order_tip: "修改顺序(移动此段落)" },
};
const T = I18N[LANG] ?? I18N.en;
app.registerExtension({
name: "PromptSegments",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== "PromptSegments") return;
nodeType.prototype._calcHeight = function () {
const LINE_H = 18, PADDING = 20, TOP_ROW = 30, SEG_GAP = 5, NODE_CHROME = 54;
let h = NODE_CHROME;
this._segments.forEach(seg => {
const lines = Math.max(1, (seg.text.match(/\n/g) || []).length + 1);
h += TOP_ROW + Math.max(64, lines * LINE_H + PADDING) + SEG_GAP;
});
return h + 36;
};
nodeType.prototype._applySize = function () {
this._domWidget.element.style.height = "auto";
this._domWidget.computeSize = () => [0, -4];
requestAnimationFrame(() => {
const domH = this._segContainer.scrollHeight;
// When the node is off-screen or zoomed too small, ComfyUI hides
// the DOM widget (display:none), which makes scrollHeight return 0.
if (!domH || domH <= 0) {
// No reliable layout info. If we already have a measured size,
// keep it (don't collapse). Otherwise fall back to a content
// based estimate so a brand-new hidden node still gets a size.
if (!this._minH) {
const h = this._calcHeight();
this._minH = h;
this.size[1] = h;
app.graph.setDirtyCanvas(true, true);
}
return;
}
const h = domH + 50;
this._minH = h;
this.size[1] = h;
app.graph.setDirtyCanvas(true, true);
});
};
// Re-measure every textarea + node size. Used when the node becomes visible
// again after having been hidden during a render (e.g. page load or undo
// while off-screen), since the initial measurement ran while hidden.
nodeType.prototype._remeasure = function () {
if (!this._segContainer) return;
this._segContainer.querySelectorAll("textarea").forEach(ta => {
ta.style.height = "auto";
const sh = ta.scrollHeight;
if (sh && sh > 0) ta.style.height = sh + "px";
});
this._applySize();
};
nodeType.prototype._render = function () {
const container = this._segContainer;
container.innerHTML = "";
this._segments.forEach((seg, i) => {
const row = document.createElement("div");
const updateRowStyle = () => {
row.style.cssText = `display:flex;flex-direction:column;gap:0;border-radius:6px;overflow:hidden;border:1px solid ${seg.enabled ? "#3a3a4a" : "#2a2a2a"};opacity:${seg.enabled ? "1" : "0.5"};transition:opacity 0.2s;`;
};
updateRowStyle();
const top = document.createElement("div");
top.style.cssText = `display:flex;align-items:center;gap:6px;padding:5px 6px;background:${seg.enabled ? "#2d2d3d" : "#252525"};border-left:3px solid ${seg.enabled ? "#4a9eff" : "#555"};`;
const toggle = document.createElement("input");
toggle.type = "checkbox"; toggle.checked = seg.enabled;
toggle.style.cssText = "width:14px;height:14px;cursor:pointer;accent-color:#4a9eff;flex-shrink:0;";
toggle.onchange = () => {
seg.enabled = toggle.checked;
updateRowStyle();top.style.background = seg.enabled ? "#2d2d3d" : "#252525";
top.style.borderLeftColor = seg.enabled ? "#4a9eff" : "#555";
row.style.borderColor = seg.enabled ? "#3a3a4a" : "#2a2a2a";
this._sync();
};
const labelNum = document.createElement("input");
labelNum.type = "text";
labelNum.value = i + 1;
labelNum.title = T.order_tip;
labelNum.style.cssText = "color:#aaa;font-size:11px;flex-shrink:0;font-family:monospace;width:26px;text-align:center;background:#1a1a22;border:1px solid #333344;border-radius:4px;padding:1px 0;outline:none;box-sizing:content-box;";
labelNum.onfocus = () => { labelNum.style.borderColor = "#4a9eff"; labelNum.style.color = "#fff"; labelNum.select(); };
labelNum.onblur = () => { labelNum.style.borderColor = "#333344"; labelNum.style.color = "#aaa"; };
labelNum.onchange = () => {
const raw = labelNum.value.trim();
const parsed = parseFloat(raw);
const total = this._segments.length;
const current = i + 1;
// Clamping (no rejection), mirroring native ComfyUI inputs
// (sampler steps, lora strength): invalid -> keep original.
if (!Number.isFinite(parsed)) { labelNum.value = current; return; }
let target = Math.round(parsed);
if (target <= 0) target = 1;
if (target > total) target = total;
if (target === current) { labelNum.value = current; return; }
// Move-insert: lift the whole segment object (text/label/enabled
// travel with it) and insert at the new position.
const item = this._segments.splice(i, 1)[0];
this._segments.splice(target - 1, 0, item);
this._sync();
this._render();
};
const label = document.createElement("input");
label.type = "text"; label.value = seg.label; label.placeholder = T.label_ph;
label.style.cssText = "flex:1;background:transparent;color:#bbb;border:none;outline:none;padding:0 2px;font-size:11px;min-width:0;";
label.oninput = () => { seg.label = label.value; };
const del = document.createElement("button");
del.innerHTML = "✕";
del.style.cssText = "background:transparent;color:#666;border:none;border-radius:3px;padding:2px 5px;cursor:pointer;font-size:11px;flex-shrink:0;line-height:1;transition:color 0.15s,background 0.15s;";
del.onmouseenter = () => { del.style.color = "#fff"; del.style.background = "#c0392b"; };
del.onmouseleave = () => { del.style.color = "#666"; del.style.background = "transparent"; };
del.onclick = () => { this._segments.splice(i, 1); this._sync(); this._render(); };
top.append(toggle, labelNum, label, del);
const ta = document.createElement("textarea");
ta.value = seg.text; ta.placeholder = T.seg_ph(i + 1);
ta.style.cssText = "width:100%;min-height:64px;height:64px;background:#181820;color:#ddd;border:none;border-top:1px solid #2a2a3a;padding:6px 8px;font-size:12px;resize:none;box-sizing:border-box;outline:none;font-family:monospace;line-height:1.5;overflow:hidden;";
ta.onfocus = () => { ta.style.background = "#1c1c28"; };
ta.onblur = () => { ta.style.background = "#181820"; };
const autoResize = () => {
ta.style.height = "auto";
// scrollHeight is 0 while the widget DOM is hidden
// (off-screen / zoomed out); skip so we don't collapse it.
const sh = ta.scrollHeight;
if (sh && sh > 0) ta.style.height = sh + "px";
this._applySize();
};
ta.oninput = () => { seg.text = ta.value; autoResize(); this._sync(); };
requestAnimationFrame(autoResize);
row.append(top, ta);
container.appendChild(row);
});
const addBtn = document.createElement("button");
addBtn.innerHTML = T.add;
addBtn.style.cssText = "width:100%;padding:6px;background:transparent;color:#4a9eff;border:1px dashed #3a5a7a;border-radius:6px;cursor:pointer;font-size:12px;transition:background 0.15s,border-color 0.15s;";
addBtn.onmouseenter = () => { addBtn.style.background = "#1a2a3a"; addBtn.style.borderColor = "#4a9eff"; };
addBtn.onmouseleave = () => { addBtn.style.background = "transparent"; addBtn.style.borderColor = "#3a5a7a"; };
addBtn.onclick = () => { this._segments.push({ enabled: true, label: "", text: "" }); this._sync(); this._render(); };
container.appendChild(addBtn);
this._applySize();
};
nodeType.prototype._sync = function () {
if (this._rawWidget) this._rawWidget.value = JSON.stringify(this._segments);
};
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
onNodeCreated?.apply(this, arguments);
const raw = this.widgets?.find(w => w.name === "segments");
if (raw) { raw.hidden = true; raw.computeSize = () => [0, -4]; }
this._rawWidget = raw;
this._segments = [{ enabled: true, label: T.default_label, text: "" }];
const container = document.createElement("div");
container.style.cssText = "display:flex;flex-direction:column;gap:4px;padding:4px;box-sizing:border-box;width:100%;";
this._segContainer = container;
this._domWidget = this.addDOMWidget("_ui", "div", container, { serialize: false });
this.onResize = function (size) {
if (this._minH && size[1] < this._minH) size[1] = this._minH;
};
// Watch for the node re-entering the viewport. If a render happened
// while the widget DOM was hidden (off-screen / heavily zoomed out,
// e.g. on page load or right after an undo), scrollHeight was 0 and
// the textareas never expanded. Re-measure on each hidden->visible
// transition. The observer stays alive for the node's lifetime so it
// also catches later undos that re-render while the node is off-screen.
this._wasVisible = false;
if ("IntersectionObserver" in window) {
this._visObserver = new IntersectionObserver((entries) => {
for (const e of entries) {
// Only act on a hidden -> visible transition, so we don't
// re-measure on every visible frame (which would be wasteful
// and could fight the user while typing / dragging).
if (e.isIntersecting && !this._wasVisible) {
this._remeasure();
}
this._wasVisible = e.isIntersecting;
}
}, { threshold: 0 });
// observe on next frame so the DOM widget element is attached
requestAnimationFrame(() => {
if (this._visObserver && this._domWidget?.element) {
this._visObserver.observe(this._domWidget.element);
}
});
}
requestAnimationFrame(() => this._render());
};
const onSerialize = nodeType.prototype.onSerialize;
nodeType.prototype.onSerialize = function (o) {
onSerialize?.apply(this, arguments);
o.prompt_segments = this._segments;
};
const onRemoved = nodeType.prototype.onRemoved;
nodeType.prototype.onRemoved = function () {
onRemoved?.apply(this, arguments);
if (this._visObserver) { this._visObserver.disconnect(); this._visObserver = null; }
};
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function (o) {
onConfigure?.apply(this, arguments);
if (o.prompt_segments) {
this._segments = o.prompt_segments;
requestAnimationFrame(() => this._render());
}
};
}
});