|
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: () => { |
|
try { |
|
|
|
if (typeof app.ui.settings.element?.close === "function") { |
|
app.ui.settings.element.close(); |
|
} |
|
} catch (error) { |
|
} |
|
try { |
|
|
|
document.querySelector(".p-dialog-close-button").click(); |
|
} catch (error) { |
|
|
|
app.ui.settings.element.style.display = "none"; |
|
} |
|
|
|
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); |
|
} |
|
} |
|
}, |
|
}); |
|
|