|
|
import { api } from "scripts/api.js";
|
|
|
import type {
|
|
|
LLink,
|
|
|
IComboWidget,
|
|
|
LGraphNode,
|
|
|
INodeOutputSlot,
|
|
|
INodeInputSlot,
|
|
|
IWidget,
|
|
|
SerializedLGraphNode,
|
|
|
} from "typings/litegraph.js";
|
|
|
import type { ComfyObjectInfo, ComfyGraphNode } from "typings/comfy.js";
|
|
|
import { wait } from "rgthree/common/shared_utils.js";
|
|
|
import { rgthree } from "./rgthree.js";
|
|
|
|
|
|
|
|
|
export class PowerPrompt {
|
|
|
readonly isSimple: boolean;
|
|
|
readonly node: ComfyGraphNode;
|
|
|
readonly promptEl: HTMLTextAreaElement;
|
|
|
nodeData: ComfyObjectInfo;
|
|
|
readonly combos: { [key: string]: IComboWidget } = {};
|
|
|
readonly combosValues: { [key: string]: string[] } = {};
|
|
|
boundOnFreshNodeDefs!: (event: CustomEvent) => void;
|
|
|
|
|
|
private configuring = false;
|
|
|
|
|
|
constructor(node: ComfyGraphNode, nodeData: ComfyObjectInfo) {
|
|
|
this.node = node;
|
|
|
this.node.properties = this.node.properties || {};
|
|
|
|
|
|
this.node.properties["combos_filter"] = "";
|
|
|
|
|
|
this.nodeData = nodeData;
|
|
|
this.isSimple = this.nodeData.name.includes("Simple");
|
|
|
|
|
|
this.promptEl = (node.widgets[0]! as any).inputEl;
|
|
|
this.addAndHandleKeyboardLoraEditWeight();
|
|
|
|
|
|
this.patchNodeRefresh();
|
|
|
|
|
|
const oldConfigure = this.node.configure;
|
|
|
this.node.configure = (info: SerializedLGraphNode) => {
|
|
|
this.configuring = true;
|
|
|
oldConfigure?.apply(this.node, [info]);
|
|
|
this.configuring = false;
|
|
|
};
|
|
|
|
|
|
const oldOnConnectionsChange = this.node.onConnectionsChange;
|
|
|
this.node.onConnectionsChange = (
|
|
|
type: number,
|
|
|
slotIndex: number,
|
|
|
isConnected: boolean,
|
|
|
link_info: LLink,
|
|
|
_ioSlot: INodeOutputSlot | INodeInputSlot,
|
|
|
) => {
|
|
|
oldOnConnectionsChange?.apply(this.node, [type, slotIndex, isConnected, link_info, _ioSlot]);
|
|
|
this.onNodeConnectionsChange(type, slotIndex, isConnected, link_info, _ioSlot);
|
|
|
};
|
|
|
|
|
|
const oldOnConnectInput = this.node.onConnectInput;
|
|
|
this.node.onConnectInput = (
|
|
|
inputIndex: number,
|
|
|
outputType: INodeOutputSlot["type"],
|
|
|
outputSlot: INodeOutputSlot,
|
|
|
outputNode: LGraphNode,
|
|
|
outputIndex: number,
|
|
|
) => {
|
|
|
let canConnect = true;
|
|
|
if (oldOnConnectInput) {
|
|
|
canConnect = oldOnConnectInput.apply(this.node, [
|
|
|
inputIndex,
|
|
|
outputType,
|
|
|
outputSlot,
|
|
|
outputNode,
|
|
|
outputIndex,
|
|
|
]);
|
|
|
}
|
|
|
return (
|
|
|
this.configuring ||
|
|
|
rgthree.loadingApiJson ||
|
|
|
(canConnect && !this.node.inputs[inputIndex]!.disabled)
|
|
|
);
|
|
|
};
|
|
|
|
|
|
const oldOnConnectOutput = this.node.onConnectOutput;
|
|
|
this.node.onConnectOutput = (
|
|
|
outputIndex: number,
|
|
|
inputType: INodeInputSlot["type"],
|
|
|
inputSlot: INodeInputSlot,
|
|
|
inputNode: LGraphNode,
|
|
|
inputIndex: number,
|
|
|
) => {
|
|
|
let canConnect = true;
|
|
|
if (oldOnConnectOutput) {
|
|
|
canConnect = oldOnConnectOutput?.apply(this.node, [
|
|
|
outputIndex,
|
|
|
inputType,
|
|
|
inputSlot,
|
|
|
inputNode,
|
|
|
inputIndex,
|
|
|
]);
|
|
|
}
|
|
|
return (
|
|
|
this.configuring ||
|
|
|
rgthree.loadingApiJson ||
|
|
|
(canConnect && !this.node.outputs[outputIndex]!.disabled)
|
|
|
);
|
|
|
};
|
|
|
|
|
|
const onPropertyChanged = this.node.onPropertyChanged;
|
|
|
this.node.onPropertyChanged = (property: string, value: any, prevValue: any) => {
|
|
|
onPropertyChanged && onPropertyChanged.call(this, property, value, prevValue);
|
|
|
if (property === "combos_filter") {
|
|
|
this.refreshCombos(this.nodeData);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = this.node.widgets.length - 1; i >= 0; i--) {
|
|
|
if (this.shouldRemoveServerWidget(this.node.widgets[i]!)) {
|
|
|
this.node.widgets.splice(i, 1);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
this.refreshCombos(nodeData);
|
|
|
setTimeout(() => {
|
|
|
this.stabilizeInputsOutputs();
|
|
|
}, 32);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onNodeConnectionsChange(
|
|
|
_type: number,
|
|
|
_slotIndex: number,
|
|
|
_isConnected: boolean,
|
|
|
_linkInfo: LLink,
|
|
|
_ioSlot: INodeOutputSlot | INodeInputSlot,
|
|
|
) {
|
|
|
this.stabilizeInputsOutputs();
|
|
|
}
|
|
|
|
|
|
private stabilizeInputsOutputs() {
|
|
|
|
|
|
|
|
|
if (this.configuring || rgthree.loadingApiJson) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const clipLinked = this.node.inputs.some((i) => i.name.includes("clip") && !!i.link);
|
|
|
const modelLinked = this.node.inputs.some((i) => i.name.includes("model") && !!i.link);
|
|
|
for (const output of this.node.outputs) {
|
|
|
const type = (output.type as string).toLowerCase();
|
|
|
if (type.includes("model")) {
|
|
|
output.disabled = !modelLinked;
|
|
|
} else if (type.includes("conditioning")) {
|
|
|
output.disabled = !clipLinked;
|
|
|
} else if (type.includes("clip")) {
|
|
|
output.disabled = !clipLinked;
|
|
|
} else if (type.includes("string")) {
|
|
|
|
|
|
|
|
|
output.color_off = "#7F7";
|
|
|
output.color_on = "#7F7";
|
|
|
}
|
|
|
if (output.disabled) {
|
|
|
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
onFreshNodeDefs(event: CustomEvent) {
|
|
|
this.refreshCombos(event.detail[this.nodeData.name]);
|
|
|
}
|
|
|
|
|
|
shouldRemoveServerWidget(widget: IWidget) {
|
|
|
return (
|
|
|
widget.name?.startsWith("insert_") ||
|
|
|
widget.name?.startsWith("target_") ||
|
|
|
widget.name?.startsWith("crop_") ||
|
|
|
widget.name?.startsWith("values_")
|
|
|
);
|
|
|
}
|
|
|
|
|
|
refreshCombos(nodeData: ComfyObjectInfo) {
|
|
|
this.nodeData = nodeData;
|
|
|
let filter: RegExp | null = null;
|
|
|
if (this.node.properties["combos_filter"]?.trim()) {
|
|
|
try {
|
|
|
filter = new RegExp(this.node.properties["combos_filter"].trim(), "i");
|
|
|
} catch (e) {
|
|
|
console.error(`Could not parse "${filter}" for Regular Expression`, e);
|
|
|
filter = null;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
let data = Object.assign(
|
|
|
{},
|
|
|
this.nodeData.input?.optional || {},
|
|
|
this.nodeData.input?.hidden || {},
|
|
|
);
|
|
|
|
|
|
for (const [key, value] of Object.entries(data)) {
|
|
|
|
|
|
if (Array.isArray(value[0])) {
|
|
|
let values = value[0] as string[];
|
|
|
if (key.startsWith("insert")) {
|
|
|
values = filter
|
|
|
? values.filter(
|
|
|
(v, i) => i < 1 || (i == 1 && v.match(/^disable\s[a-z]/i)) || filter?.test(v),
|
|
|
)
|
|
|
: values;
|
|
|
const shouldShow =
|
|
|
values.length > 2 || (values.length > 1 && !values[1]!.match(/^disable\s[a-z]/i));
|
|
|
if (shouldShow) {
|
|
|
if (!this.combos[key]) {
|
|
|
this.combos[key] = this.node.addWidget(
|
|
|
"combo",
|
|
|
key,
|
|
|
values,
|
|
|
(selected) => {
|
|
|
if (selected !== values[0] && !selected.match(/^disable\s[a-z]/i)) {
|
|
|
|
|
|
|
|
|
wait().then(() => {
|
|
|
if (key.includes("embedding")) {
|
|
|
this.insertSelectionText(`embedding:${selected}`);
|
|
|
} else if (key.includes("saved")) {
|
|
|
this.insertSelectionText(
|
|
|
this.combosValues[`values_${key}`]![values.indexOf(selected)]!,
|
|
|
);
|
|
|
} else if (key.includes("lora")) {
|
|
|
this.insertSelectionText(`<lora:${selected}:1.0>`);
|
|
|
}
|
|
|
this.combos[key]!.value = values[0];
|
|
|
});
|
|
|
}
|
|
|
},
|
|
|
{
|
|
|
values,
|
|
|
serialize: true,
|
|
|
},
|
|
|
);
|
|
|
(this.combos[key]! as any).oldComputeSize = this.combos[key]!.computeSize;
|
|
|
let node = this.node;
|
|
|
this.combos[key]!.computeSize = function (width: number) {
|
|
|
const size = (this as any).oldComputeSize?.(width) || [
|
|
|
width,
|
|
|
LiteGraph.NODE_WIDGET_HEIGHT,
|
|
|
];
|
|
|
if (this === node.widgets[node.widgets.length - 1]) {
|
|
|
size[1] += 10;
|
|
|
}
|
|
|
return size;
|
|
|
};
|
|
|
}
|
|
|
this.combos[key]!.options!.values = values;
|
|
|
this.combos[key]!.value = values[0];
|
|
|
} else if (!shouldShow && this.combos[key]) {
|
|
|
this.node.widgets.splice(this.node.widgets.indexOf(this.combos[key]!), 1);
|
|
|
delete this.combos[key];
|
|
|
}
|
|
|
} else if (key.startsWith("values")) {
|
|
|
this.combosValues[key] = values;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
insertSelectionText(text: string) {
|
|
|
if (!this.promptEl) {
|
|
|
console.error("Asked to insert text, but no textbox found.");
|
|
|
return;
|
|
|
}
|
|
|
let prompt = this.promptEl.value;
|
|
|
|
|
|
|
|
|
let first = prompt.substring(0, this.promptEl.selectionEnd).replace(/ +$/, "");
|
|
|
first = first + (["\n"].includes(first[first.length - 1]!) ? "" : first.length ? " " : "");
|
|
|
let second = prompt.substring(this.promptEl.selectionEnd).replace(/^ +/, "");
|
|
|
second = (["\n"].includes(second[0]!) ? "" : second.length ? " " : "") + second;
|
|
|
this.promptEl.value = first + text + second;
|
|
|
this.promptEl.focus();
|
|
|
this.promptEl.selectionStart = first.length;
|
|
|
this.promptEl.selectionEnd = first.length + text.length;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
addAndHandleKeyboardLoraEditWeight() {
|
|
|
this.promptEl.addEventListener("keydown", (event: KeyboardEvent) => {
|
|
|
|
|
|
if (!(event.key === "ArrowUp" || event.key === "ArrowDown")) return;
|
|
|
if (!event.ctrlKey && !event.metaKey) return;
|
|
|
|
|
|
|
|
|
const delta = event.shiftKey ? 0.01 : 0.1;
|
|
|
|
|
|
let start = this.promptEl.selectionStart;
|
|
|
let end = this.promptEl.selectionEnd;
|
|
|
let fullText = this.promptEl.value;
|
|
|
let selectedText = fullText.substring(start, end);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!selectedText) {
|
|
|
const stopOn = "<>()\r\n\t";
|
|
|
if (fullText[start] == ">") {
|
|
|
start -= 2;
|
|
|
end -= 2;
|
|
|
}
|
|
|
if (fullText[end - 1] == "<") {
|
|
|
start += 2;
|
|
|
end += 2;
|
|
|
}
|
|
|
while (!stopOn.includes(fullText[start]!) && start > 0) {
|
|
|
start--;
|
|
|
}
|
|
|
while (!stopOn.includes(fullText[end - 1]!) && end < fullText.length) {
|
|
|
end++;
|
|
|
}
|
|
|
selectedText = fullText.substring(start, end);
|
|
|
}
|
|
|
|
|
|
|
|
|
if (!selectedText.startsWith("<lora:") || !selectedText.endsWith(">")) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
let weight = Number(selectedText.match(/:(-?\d*(\.\d*)?)>$/)?.[1]) ?? 1;
|
|
|
weight += event.key === "ArrowUp" ? delta : -delta;
|
|
|
const updatedText = selectedText.replace(/(:-?\d*(\.\d*)?)?>$/, `:${weight.toFixed(2)}>`);
|
|
|
|
|
|
|
|
|
this.promptEl.setRangeText(updatedText, start, end, "select");
|
|
|
event.preventDefault();
|
|
|
event.stopPropagation();
|
|
|
});
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
patchNodeRefresh() {
|
|
|
this.boundOnFreshNodeDefs = this.onFreshNodeDefs.bind(this);
|
|
|
api.addEventListener("fresh-node-defs", this.boundOnFreshNodeDefs as EventListener);
|
|
|
const oldNodeRemoved = this.node.onRemoved;
|
|
|
this.node.onRemoved = () => {
|
|
|
oldNodeRemoved?.call(this.node);
|
|
|
api.removeEventListener("fresh-node-defs", this.boundOnFreshNodeDefs as EventListener);
|
|
|
};
|
|
|
}
|
|
|
}
|
|
|
|