342 lines
7.5 KiB
JavaScript
342 lines
7.5 KiB
JavaScript
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([]); // Initialize the first row
|
|
|
|
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++; // Handle Windows line endings (\r\n)
|
|
}
|
|
rows.push([]); // Start a new row
|
|
} else {
|
|
currentField += char;
|
|
}
|
|
} else {
|
|
if (char === quote && nextChar === quote) {
|
|
currentField += quote;
|
|
i++; // Skip the next quote
|
|
} else if (char === quote) {
|
|
inQuotedField = false;
|
|
} else {
|
|
currentField += char;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove the last row if it's empty
|
|
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:
|
|
// Single word
|
|
text = n[0];
|
|
break;
|
|
case 2:
|
|
// Word,[priority|alias]
|
|
num = +n[1];
|
|
if (isNaN(num)) {
|
|
text = n[1];
|
|
value = n[0];
|
|
} else {
|
|
text = n[0];
|
|
priority = num;
|
|
}
|
|
break;
|
|
default:
|
|
// Word,alias,priority
|
|
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;
|
|
ComfyWidgets.STRING = function (node, inputName, inputData) {
|
|
const r = STRING.apply(this, arguments);
|
|
|
|
if (inputData[1]?.multiline && inputData[1]?.["pysssss.autocomplete"] !== false) {
|
|
new TextAreaAutoComplete(r.widget.inputEl);
|
|
}
|
|
|
|
return r;
|
|
};
|
|
|
|
TextAreaAutoComplete.separator = 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.separator,
|
|
onchange: (event) => {
|
|
const checked = !!event.target.checked;
|
|
TextAreaAutoComplete.separator = checked ? ", " : "";
|
|
localStorage.setItem(id + ".AutoSeparate", TextAreaAutoComplete.separator);
|
|
},
|
|
}),
|
|
]
|
|
),
|
|
$el("button", {
|
|
textContent: "Manage Custom Words",
|
|
onclick: () => {
|
|
app.ui.settings.element.close();
|
|
new CustomWordsDialog().show();
|
|
},
|
|
style: {
|
|
fontSize: "14px",
|
|
display: "block",
|
|
marginTop: "5px",
|
|
},
|
|
}),
|
|
]),
|
|
]);
|
|
},
|
|
});
|
|
},
|
|
});
|