|
import { app } from "../../../scripts/app.js";
|
|
import { ComfyWidgets } from "../../../scripts/widgets.js";
|
|
import { api } from "../../../scripts/api.js";
|
|
import { $el, ComfyDialog } from "../../../scripts/ui.js";
|
|
import { TextAreaAutoComplete } from "./common/autocomplete.js";
|
|
import { ModelInfoDialog } from "./common/modelInfoDialog.js";
|
|
import { LoraInfoDialog } from "./modelInfo.js";
|
|
|
|
function parseCSV(csvText) {
|
|
const rows = [];
|
|
const delimiter = ",";
|
|
const quote = '"';
|
|
let currentField = "";
|
|
let inQuotedField = false;
|
|
|
|
function pushField() {
|
|
rows[rows.length - 1].push(currentField);
|
|
currentField = "";
|
|
inQuotedField = false;
|
|
}
|
|
|
|
rows.push([]);
|
|
|
|
for (let i = 0; i < csvText.length; i++) {
|
|
const char = csvText[i];
|
|
const nextChar = csvText[i + 1];
|
|
|
|
|
|
if (char === "\\" && nextChar === quote) {
|
|
currentField += quote;
|
|
i++;
|
|
}
|
|
|
|
if (!inQuotedField) {
|
|
if (char === quote) {
|
|
inQuotedField = true;
|
|
} else if (char === delimiter) {
|
|
pushField();
|
|
} else if (char === "\r" || char === "\n" || i === csvText.length - 1) {
|
|
pushField();
|
|
if (nextChar === "\n") {
|
|
i++;
|
|
}
|
|
rows.push([]);
|
|
} else {
|
|
currentField += char;
|
|
}
|
|
} else {
|
|
if (char === quote && nextChar === quote) {
|
|
currentField += quote;
|
|
i++;
|
|
} else if (char === quote) {
|
|
inQuotedField = false;
|
|
} else if (char === "\r" || char === "\n" || i === csvText.length - 1) {
|
|
|
|
const parsed = parseCSV(currentField);
|
|
rows.pop();
|
|
rows.push(...parsed);
|
|
inQuotedField = false;
|
|
currentField = "";
|
|
rows.push([]);
|
|
} else {
|
|
currentField += char;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (currentField || csvText[csvText.length - 1] === ",") {
|
|
pushField();
|
|
}
|
|
|
|
|
|
if (rows[rows.length - 1].length === 0) {
|
|
rows.pop();
|
|
}
|
|
|
|
return rows;
|
|
}
|
|
|
|
async function getCustomWords() {
|
|
const resp = await api.fetchApi("/pysssss/autocomplete", { cache: "no-store" });
|
|
if (resp.status === 200) {
|
|
return await resp.text();
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
async function addCustomWords(text) {
|
|
if (!text) {
|
|
text = await getCustomWords();
|
|
}
|
|
if (text) {
|
|
TextAreaAutoComplete.updateWords(
|
|
"pysssss.customwords",
|
|
parseCSV(text).reduce((p, n) => {
|
|
let text;
|
|
let priority;
|
|
let value;
|
|
let num;
|
|
switch (n.length) {
|
|
case 0:
|
|
return;
|
|
case 1:
|
|
|
|
text = n[0];
|
|
break;
|
|
case 2:
|
|
|
|
num = +n[1];
|
|
if (isNaN(num)) {
|
|
text = n[0] + "🔄️" + n[1];
|
|
value = n[0];
|
|
} else {
|
|
text = n[0];
|
|
priority = num;
|
|
}
|
|
break;
|
|
case 4:
|
|
|
|
value = n[0];
|
|
priority = +n[2];
|
|
const aliases = n[3]?.trim();
|
|
if (aliases && aliases !== "null") {
|
|
const split = aliases.split(",");
|
|
for (const text of split) {
|
|
p[text] = { text, priority, value };
|
|
}
|
|
}
|
|
text = value;
|
|
break;
|
|
default:
|
|
|
|
text = n[1];
|
|
value = n[0];
|
|
priority = +n[2];
|
|
break;
|
|
}
|
|
p[text] = { text, priority, value };
|
|
return p;
|
|
}, {})
|
|
);
|
|
}
|
|
}
|
|
|
|
function toggleLoras() {
|
|
[TextAreaAutoComplete.globalWords, TextAreaAutoComplete.globalWordsExclLoras] = [
|
|
TextAreaAutoComplete.globalWordsExclLoras,
|
|
TextAreaAutoComplete.globalWords,
|
|
];
|
|
}
|
|
|
|
class EmbeddingInfoDialog extends ModelInfoDialog {
|
|
async addInfo() {
|
|
super.addInfo();
|
|
const info = await this.addCivitaiInfo();
|
|
if (info) {
|
|
$el("div", {
|
|
parent: this.content,
|
|
innerHTML: info.description,
|
|
style: {
|
|
maxHeight: "250px",
|
|
overflow: "auto",
|
|
},
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
class CustomWordsDialog extends ComfyDialog {
|
|
async show() {
|
|
const text = await getCustomWords();
|
|
this.words = $el("textarea", {
|
|
textContent: text,
|
|
style: {
|
|
width: "70vw",
|
|
height: "70vh",
|
|
},
|
|
});
|
|
|
|
const input = $el("input", {
|
|
style: {
|
|
flex: "auto",
|
|
},
|
|
value:
|
|
"https://gist.githubusercontent.com/pythongosssss/1d3efa6050356a08cea975183088159a/raw/a18fb2f94f9156cf4476b0c24a09544d6c0baec6/danbooru-tags.txt",
|
|
});
|
|
|
|
super.show(
|
|
$el(
|
|
"div",
|
|
{
|
|
style: {
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
overflow: "hidden",
|
|
maxHeight: "100%",
|
|
},
|
|
},
|
|
[
|
|
$el("h2", {
|
|
textContent: "Custom Autocomplete Words",
|
|
style: {
|
|
color: "#fff",
|
|
marginTop: 0,
|
|
textAlign: "center",
|
|
fontFamily: "sans-serif",
|
|
},
|
|
}),
|
|
$el(
|
|
"div",
|
|
{
|
|
style: {
|
|
color: "#fff",
|
|
fontFamily: "sans-serif",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: "5px",
|
|
},
|
|
},
|
|
[
|
|
$el("label", { textContent: "Load Custom List: " }),
|
|
input,
|
|
$el("button", {
|
|
textContent: "Load",
|
|
onclick: async () => {
|
|
try {
|
|
const res = await fetch(input.value);
|
|
if (res.status !== 200) {
|
|
throw new Error("Error loading: " + res.status + " " + res.statusText);
|
|
}
|
|
this.words.value = await res.text();
|
|
} catch (error) {
|
|
alert("Error loading custom list, try manually copy + pasting the list");
|
|
}
|
|
},
|
|
}),
|
|
]
|
|
),
|
|
this.words,
|
|
]
|
|
)
|
|
);
|
|
}
|
|
|
|
createButtons() {
|
|
const btns = super.createButtons();
|
|
const save = $el("button", {
|
|
type: "button",
|
|
textContent: "Save",
|
|
onclick: async (e) => {
|
|
try {
|
|
const res = await api.fetchApi("/pysssss/autocomplete", { method: "POST", body: this.words.value });
|
|
if (res.status !== 200) {
|
|
throw new Error("Error saving: " + res.status + " " + res.statusText);
|
|
}
|
|
save.textContent = "Saved!";
|
|
addCustomWords(this.words.value);
|
|
setTimeout(() => {
|
|
save.textContent = "Save";
|
|
}, 500);
|
|
} catch (error) {
|
|
alert("Error saving word list!");
|
|
console.error(error);
|
|
}
|
|
},
|
|
});
|
|
|
|
btns.unshift(save);
|
|
return btns;
|
|
}
|
|
}
|
|
|
|
const id = "pysssss.AutoCompleter";
|
|
|
|
app.registerExtension({
|
|
name: id,
|
|
init() {
|
|
const STRING = ComfyWidgets.STRING;
|
|
const SKIP_WIDGETS = new Set(["ttN xyPlot.x_values", "ttN xyPlot.y_values"]);
|
|
ComfyWidgets.STRING = function (node, inputName, inputData) {
|
|
const r = STRING.apply(this, arguments);
|
|
|
|
if (inputData[1]?.multiline) {
|
|
|
|
const config = inputData[1]?.["pysssss.autocomplete"];
|
|
if (config === false) return r;
|
|
|
|
|
|
const id = `${node.comfyClass}.${inputName}`;
|
|
if (SKIP_WIDGETS.has(id)) return r;
|
|
|
|
let words;
|
|
let separator;
|
|
if (typeof config === "object") {
|
|
separator = config.separator;
|
|
words = {};
|
|
if (config.words) {
|
|
|
|
Object.assign(words, TextAreaAutoComplete.groups[node.comfyClass + "." + inputName] ?? {});
|
|
}
|
|
|
|
for (const item of config.groups ?? []) {
|
|
if (item === "*") {
|
|
|
|
Object.assign(words, TextAreaAutoComplete.globalWords);
|
|
} else {
|
|
|
|
Object.assign(words, TextAreaAutoComplete.groups[item] ?? {});
|
|
}
|
|
}
|
|
}
|
|
|
|
new TextAreaAutoComplete(r.widget.inputEl, words, separator);
|
|
}
|
|
|
|
return r;
|
|
};
|
|
|
|
TextAreaAutoComplete.globalSeparator = localStorage.getItem(id + ".AutoSeparate") ?? ", ";
|
|
const enabledSetting = app.ui.settings.addSetting({
|
|
id,
|
|
name: "🐍 Text Autocomplete",
|
|
defaultValue: true,
|
|
type: (name, setter, value) => {
|
|
return $el("tr", [
|
|
$el("td", [
|
|
$el("label", {
|
|
for: id.replaceAll(".", "-"),
|
|
textContent: name,
|
|
}),
|
|
]),
|
|
$el("td", [
|
|
$el(
|
|
"label",
|
|
{
|
|
textContent: "Enabled ",
|
|
style: {
|
|
display: "block",
|
|
},
|
|
},
|
|
[
|
|
$el("input", {
|
|
id: id.replaceAll(".", "-"),
|
|
type: "checkbox",
|
|
checked: value,
|
|
onchange: (event) => {
|
|
const checked = !!event.target.checked;
|
|
TextAreaAutoComplete.enabled = checked;
|
|
setter(checked);
|
|
},
|
|
}),
|
|
]
|
|
),
|
|
$el(
|
|
"label.comfy-tooltip-indicator",
|
|
{
|
|
title: "This requires other ComfyUI nodes/extensions that support using LoRAs in the prompt.",
|
|
textContent: "Loras enabled ",
|
|
style: {
|
|
display: "block",
|
|
},
|
|
},
|
|
[
|
|
$el("input", {
|
|
type: "checkbox",
|
|
checked: !!TextAreaAutoComplete.lorasEnabled,
|
|
onchange: (event) => {
|
|
const checked = !!event.target.checked;
|
|
TextAreaAutoComplete.lorasEnabled = checked;
|
|
toggleLoras();
|
|
localStorage.setItem(id + ".ShowLoras", TextAreaAutoComplete.lorasEnabled);
|
|
},
|
|
}),
|
|
]
|
|
),
|
|
$el(
|
|
"label",
|
|
{
|
|
textContent: "Auto-insert comma ",
|
|
style: {
|
|
display: "block",
|
|
},
|
|
},
|
|
[
|
|
$el("input", {
|
|
type: "checkbox",
|
|
checked: !!TextAreaAutoComplete.globalSeparator,
|
|
onchange: (event) => {
|
|
const checked = !!event.target.checked;
|
|
TextAreaAutoComplete.globalSeparator = checked ? ", " : "";
|
|
localStorage.setItem(id + ".AutoSeparate", TextAreaAutoComplete.globalSeparator);
|
|
},
|
|
}),
|
|
]
|
|
),
|
|
$el(
|
|
"label",
|
|
{
|
|
textContent: "Replace _ with space ",
|
|
style: {
|
|
display: "block",
|
|
},
|
|
},
|
|
[
|
|
$el("input", {
|
|
type: "checkbox",
|
|
checked: !!TextAreaAutoComplete.replacer,
|
|
onchange: (event) => {
|
|
const checked = !!event.target.checked;
|
|
TextAreaAutoComplete.replacer = checked ? (v) => v.replaceAll("_", " ") : undefined;
|
|
localStorage.setItem(id + ".ReplaceUnderscore", checked);
|
|
},
|
|
}),
|
|
]
|
|
),
|
|
$el(
|
|
"label",
|
|
{
|
|
textContent: "Insert suggestion on: ",
|
|
style: {
|
|
display: "block",
|
|
},
|
|
},
|
|
[
|
|
$el(
|
|
"label",
|
|
{
|
|
textContent: "Tab",
|
|
style: {
|
|
display: "block",
|
|
marginLeft: "20px",
|
|
},
|
|
},
|
|
[
|
|
$el("input", {
|
|
type: "checkbox",
|
|
checked: !!TextAreaAutoComplete.insertOnTab,
|
|
onchange: (event) => {
|
|
const checked = !!event.target.checked;
|
|
TextAreaAutoComplete.insertOnTab = checked;
|
|
localStorage.setItem(id + ".InsertOnTab", checked);
|
|
},
|
|
}),
|
|
]
|
|
),
|
|
$el(
|
|
"label",
|
|
{
|
|
textContent: "Enter",
|
|
style: {
|
|
display: "block",
|
|
marginLeft: "20px",
|
|
},
|
|
},
|
|
[
|
|
$el("input", {
|
|
type: "checkbox",
|
|
checked: !!TextAreaAutoComplete.insertOnEnter,
|
|
onchange: (event) => {
|
|
const checked = !!event.target.checked;
|
|
TextAreaAutoComplete.insertOnEnter = checked;
|
|
localStorage.setItem(id + ".InsertOnEnter", checked);
|
|
},
|
|
}),
|
|
]
|
|
),
|
|
]
|
|
),
|
|
$el(
|
|
"label",
|
|
{
|
|
textContent: "Max suggestions: ",
|
|
style: {
|
|
display: "block",
|
|
},
|
|
},
|
|
[
|
|
$el("input", {
|
|
type: "number",
|
|
value: +TextAreaAutoComplete.suggestionCount,
|
|
style: {
|
|
width: "80px"
|
|
},
|
|
onchange: (event) => {
|
|
const value = +event.target.value;
|
|
TextAreaAutoComplete.suggestionCount = value;;
|
|
localStorage.setItem(id + ".SuggestionCount", TextAreaAutoComplete.suggestionCount);
|
|
},
|
|
}),
|
|
]
|
|
),
|
|
$el("button", {
|
|
textContent: "Manage Custom Words",
|
|
onclick: () => {
|
|
app.ui.settings.element.close();
|
|
new CustomWordsDialog().show();
|
|
},
|
|
style: {
|
|
fontSize: "14px",
|
|
display: "block",
|
|
marginTop: "5px",
|
|
},
|
|
}),
|
|
]),
|
|
]);
|
|
},
|
|
});
|
|
|
|
TextAreaAutoComplete.enabled = enabledSetting.value;
|
|
TextAreaAutoComplete.replacer = localStorage.getItem(id + ".ReplaceUnderscore") === "true" ? (v) => v.replaceAll("_", " ") : undefined;
|
|
TextAreaAutoComplete.insertOnTab = localStorage.getItem(id + ".InsertOnTab") !== "false";
|
|
TextAreaAutoComplete.insertOnEnter = localStorage.getItem(id + ".InsertOnEnter") !== "false";
|
|
TextAreaAutoComplete.lorasEnabled = localStorage.getItem(id + ".ShowLoras") === "true";
|
|
TextAreaAutoComplete.suggestionCount = +localStorage.getItem(id + ".SuggestionCount") || 20;
|
|
},
|
|
setup() {
|
|
async function addEmbeddings() {
|
|
const embeddings = await api.getEmbeddings();
|
|
const words = {};
|
|
words["embedding:"] = { text: "embedding:" };
|
|
|
|
for (const emb of embeddings) {
|
|
const v = `embedding:${emb}`;
|
|
words[v] = {
|
|
text: v,
|
|
info: () => new EmbeddingInfoDialog(emb).show("embeddings", emb),
|
|
use_replacer: false,
|
|
};
|
|
}
|
|
|
|
TextAreaAutoComplete.updateWords("pysssss.embeddings", words);
|
|
}
|
|
|
|
async function addLoras() {
|
|
let loras;
|
|
try {
|
|
loras = LiteGraph.registered_node_types["LoraLoader"]?.nodeData.input.required.lora_name[0];
|
|
} catch (error) {}
|
|
|
|
if (!loras?.length) {
|
|
loras = await api.fetchApi("/pysssss/loras", { cache: "no-store" }).then((res) => res.json());
|
|
}
|
|
|
|
const words = {};
|
|
words["lora:"] = { text: "lora:" };
|
|
|
|
for (const lora of loras) {
|
|
const v = `<lora:${lora}:1.0>`;
|
|
words[v] = {
|
|
text: v,
|
|
info: () => new LoraInfoDialog(lora).show("loras", lora),
|
|
use_replacer: false,
|
|
};
|
|
}
|
|
|
|
TextAreaAutoComplete.updateWords("pysssss.loras", words);
|
|
}
|
|
|
|
|
|
Promise.all([addEmbeddings(), addCustomWords()])
|
|
.then(() => {
|
|
TextAreaAutoComplete.globalWordsExclLoras = Object.assign({}, TextAreaAutoComplete.globalWords);
|
|
})
|
|
.then(addLoras)
|
|
.then(() => {
|
|
if (!TextAreaAutoComplete.lorasEnabled) {
|
|
toggleLoras();
|
|
}
|
|
});
|
|
},
|
|
beforeRegisterNodeDef(_, def) {
|
|
|
|
|
|
const inputs = { ...def.input?.required, ...def.input?.optional };
|
|
for (const input in inputs) {
|
|
const config = inputs[input][1]?.["pysssss.autocomplete"];
|
|
if (!config) continue;
|
|
if (typeof config === "object" && config.words) {
|
|
const words = {};
|
|
for (const text of config.words || []) {
|
|
const obj = typeof text === "string" ? { text } : text;
|
|
words[obj.text] = obj;
|
|
}
|
|
TextAreaAutoComplete.updateWords(def.name + "." + input, words, false);
|
|
}
|
|
}
|
|
},
|
|
});
|
|
|