| 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"; |
|
|
| 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 (!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 { |
| 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[1]; |
| value = n[0]; |
| } else { |
| text = n[0]; |
| priority = num; |
| } |
| break; |
| case 4: |
| |
| value = n[0]; |
| priority = +n[2]; |
| const aliases = n[3]; |
| if (aliases) { |
| const split = aliases.split(","); |
| for (const text of split) { |
| p[text] = { text, priority, value }; |
| } |
| } |
| text = value; |
| default: |
| |
| text = n[1]; |
| value = n[0]; |
| priority = +n[2]; |
| break; |
| } |
| p[text] = { text, priority, value }; |
| return p; |
| }, {}) |
| ); |
| } |
| } |
|
|
| 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() { |
| 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), |
| }; |
| } |
|
|
| TextAreaAutoComplete.updateWords("pysssss.embeddings", words); |
| } |
|
|
| Promise.all([addEmbeddings(), addCustomWords()]); |
|
|
| 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") ?? ", "; |
| 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", |
| { |
| textContent: "Auto-insert comma ", |
| style: { |
| display: "block", |
| }, |
| }, |
| [ |
| $el("input", { |
| id: id.replaceAll(".", "-"), |
| type: "checkbox", |
| checked: !!TextAreaAutoComplete.globalSeparator, |
| onchange: (event) => { |
| const checked = !!event.target.checked; |
| TextAreaAutoComplete.globalSeparator = checked ? ", " : ""; |
| localStorage.setItem(id + ".AutoSeparate", TextAreaAutoComplete.globalSeparator); |
| }, |
| }), |
| ] |
| ), |
| $el("button", { |
| textContent: "Manage Custom Words", |
| onclick: () => { |
| app.ui.settings.element.close(); |
| new CustomWordsDialog().show(); |
| }, |
| style: { |
| fontSize: "14px", |
| display: "block", |
| marginTop: "5px", |
| }, |
| }), |
| ]), |
| ]); |
| }, |
| }); |
| }, |
| 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); |
| } |
| } |
| }, |
| }); |
|
|