|
import { $el, ComfyDialog } from "../../../../scripts/ui.js";
|
|
import { api } from "../../../../scripts/api.js";
|
|
import { addStylesheet } from "./utils.js";
|
|
|
|
addStylesheet(import.meta.url);
|
|
|
|
class MetadataDialog extends ComfyDialog {
|
|
constructor() {
|
|
super();
|
|
|
|
this.element.classList.add("pysssss-model-metadata");
|
|
}
|
|
show(metadata) {
|
|
super.show(
|
|
$el(
|
|
"div",
|
|
Object.keys(metadata).map((k) =>
|
|
$el("div", [
|
|
$el("label", { textContent: k }),
|
|
$el("span", { textContent: typeof metadata[k] === "object" ? JSON.stringify(metadata[k]) : metadata[k] }),
|
|
])
|
|
)
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
export class ModelInfoDialog extends ComfyDialog {
|
|
constructor(name, node) {
|
|
super();
|
|
this.name = name;
|
|
this.node = node;
|
|
this.element.classList.add("pysssss-model-info");
|
|
}
|
|
|
|
get customNotes() {
|
|
return this.metadata["pysssss.notes"];
|
|
}
|
|
|
|
set customNotes(v) {
|
|
this.metadata["pysssss.notes"] = v;
|
|
}
|
|
|
|
get hash() {
|
|
return this.metadata["pysssss.sha256"];
|
|
}
|
|
|
|
async show(type, value) {
|
|
this.type = type;
|
|
|
|
const req = api.fetchApi("/pysssss/metadata/" + encodeURIComponent(`${type}/${value}`));
|
|
this.info = $el("div", { style: { flex: "auto" } });
|
|
this.img = $el("img", { style: { display: "none" } });
|
|
this.imgWrapper = $el("div.pysssss-preview", [this.img]);
|
|
this.main = $el("main", { style: { display: "flex" } }, [this.info, this.imgWrapper]);
|
|
this.content = $el("div.pysssss-model-content", [$el("h2", { textContent: this.name }), this.main]);
|
|
|
|
const loading = $el("div", { textContent: "ℹ️ Loading...", parent: this.content });
|
|
|
|
super.show(this.content);
|
|
|
|
this.metadata = await (await req).json();
|
|
this.viewMetadata.style.cursor = this.viewMetadata.style.opacity = "";
|
|
this.viewMetadata.removeAttribute("disabled");
|
|
|
|
loading.remove();
|
|
this.addInfo();
|
|
}
|
|
|
|
createButtons() {
|
|
const btns = super.createButtons();
|
|
this.viewMetadata = $el("button", {
|
|
type: "button",
|
|
textContent: "View raw metadata",
|
|
disabled: "disabled",
|
|
style: {
|
|
opacity: 0.5,
|
|
cursor: "not-allowed",
|
|
},
|
|
onclick: (e) => {
|
|
if (this.metadata) {
|
|
new MetadataDialog().show(this.metadata);
|
|
}
|
|
},
|
|
});
|
|
|
|
btns.unshift(this.viewMetadata);
|
|
return btns;
|
|
}
|
|
|
|
getNoteInfo() {
|
|
function parseNote() {
|
|
if (!this.customNotes) return [];
|
|
|
|
let notes = [];
|
|
|
|
const r = new RegExp("(\\bhttps?:\\/\\/[^\\s]+)", "g");
|
|
let end = 0;
|
|
let m;
|
|
do {
|
|
m = r.exec(this.customNotes);
|
|
let pos;
|
|
let fin = 0;
|
|
if (m) {
|
|
pos = m.index;
|
|
fin = m.index + m[0].length;
|
|
} else {
|
|
pos = this.customNotes.length;
|
|
}
|
|
|
|
let pre = this.customNotes.substring(end, pos);
|
|
if (pre) {
|
|
pre = pre.replaceAll("\n", "<br>");
|
|
notes.push(
|
|
$el("span", {
|
|
innerHTML: pre,
|
|
})
|
|
);
|
|
}
|
|
if (m) {
|
|
notes.push(
|
|
$el("a", {
|
|
href: m[0],
|
|
textContent: m[0],
|
|
target: "_blank",
|
|
})
|
|
);
|
|
}
|
|
|
|
end = fin;
|
|
} while (m);
|
|
return notes;
|
|
}
|
|
|
|
let textarea;
|
|
let notesContainer;
|
|
const editText = "✏️ Edit";
|
|
const edit = $el("a", {
|
|
textContent: editText,
|
|
href: "#",
|
|
style: {
|
|
float: "right",
|
|
color: "greenyellow",
|
|
textDecoration: "none",
|
|
},
|
|
onclick: async (e) => {
|
|
e.preventDefault();
|
|
|
|
if (textarea) {
|
|
this.customNotes = textarea.value;
|
|
|
|
const resp = await api.fetchApi("/pysssss/metadata/notes/" + encodeURIComponent(`${this.type}/${this.name}`), {
|
|
method: "POST",
|
|
body: this.customNotes,
|
|
});
|
|
|
|
if (resp.status !== 200) {
|
|
console.error(resp);
|
|
alert(`Error saving notes (${req.status}) ${req.statusText}`);
|
|
return;
|
|
}
|
|
|
|
e.target.textContent = editText;
|
|
textarea.remove();
|
|
textarea = null;
|
|
|
|
notesContainer.replaceChildren(...parseNote.call(this));
|
|
this.node?.["pysssss.updateExamples"]?.();
|
|
} else {
|
|
e.target.textContent = "💾 Save";
|
|
textarea = $el("textarea", {
|
|
style: {
|
|
width: "100%",
|
|
minWidth: "200px",
|
|
minHeight: "50px",
|
|
},
|
|
textContent: this.customNotes,
|
|
});
|
|
e.target.after(textarea);
|
|
notesContainer.replaceChildren();
|
|
textarea.style.height = Math.min(textarea.scrollHeight, 300) + "px";
|
|
}
|
|
},
|
|
});
|
|
|
|
notesContainer = $el("div.pysssss-model-notes", parseNote.call(this));
|
|
return $el(
|
|
"div",
|
|
{
|
|
style: { display: "contents" },
|
|
},
|
|
[edit, notesContainer]
|
|
);
|
|
}
|
|
|
|
addInfo() {
|
|
const usageHint = this.metadata["modelspec.usage_hint"];
|
|
if (usageHint) {
|
|
this.addInfoEntry("Usage Hint", usageHint);
|
|
}
|
|
this.addInfoEntry("Notes", this.getNoteInfo());
|
|
}
|
|
|
|
addInfoEntry(name, value) {
|
|
return $el(
|
|
"p",
|
|
{
|
|
parent: this.info,
|
|
},
|
|
[
|
|
typeof name === "string" ? $el("label", { textContent: name + ": " }) : name,
|
|
typeof value === "string" ? $el("span", { textContent: value }) : value,
|
|
]
|
|
);
|
|
}
|
|
|
|
async getCivitaiDetails() {
|
|
const req = await fetch("https://civitai.com/api/v1/model-versions/by-hash/" + this.hash);
|
|
if (req.status === 200) {
|
|
return await req.json();
|
|
} else if (req.status === 404) {
|
|
throw new Error("Model not found");
|
|
} else {
|
|
throw new Error(`Error loading info (${req.status}) ${req.statusText}`);
|
|
}
|
|
}
|
|
|
|
addCivitaiInfo() {
|
|
const promise = this.getCivitaiDetails();
|
|
const content = $el("span", { textContent: "ℹ️ Loading..." });
|
|
|
|
this.addInfoEntry(
|
|
$el("label", [
|
|
$el("img", {
|
|
style: {
|
|
width: "18px",
|
|
position: "relative",
|
|
top: "3px",
|
|
margin: "0 5px 0 0",
|
|
},
|
|
src: "https://civitai.com/favicon.ico",
|
|
}),
|
|
$el("span", { textContent: "Civitai: " }),
|
|
]),
|
|
content
|
|
);
|
|
|
|
return promise
|
|
.then((info) => {
|
|
content.replaceChildren(
|
|
$el("a", {
|
|
href: "https://civitai.com/models/" + info.modelId,
|
|
textContent: "View " + info.model.name,
|
|
target: "_blank",
|
|
})
|
|
);
|
|
|
|
const allPreviews = info.images?.filter((i) => i.type === "image");
|
|
const previews = allPreviews?.filter((i) => i.nsfwLevel <= ModelInfoDialog.nsfwLevel);
|
|
if (previews?.length) {
|
|
let previewIndex = 0;
|
|
let preview;
|
|
const updatePreview = () => {
|
|
preview = previews[previewIndex];
|
|
this.img.src = preview.url;
|
|
};
|
|
|
|
updatePreview();
|
|
this.img.style.display = "";
|
|
|
|
this.img.title = `${previews.length} previews.`;
|
|
if (allPreviews.length !== previews.length) {
|
|
this.img.title += ` ${allPreviews.length - previews.length} images hidden due to NSFW level.`;
|
|
}
|
|
|
|
this.imgSave = $el("button", {
|
|
textContent: "Use as preview",
|
|
parent: this.imgWrapper,
|
|
onclick: async () => {
|
|
|
|
const blob = await (await fetch(this.img.src)).blob();
|
|
|
|
|
|
const name = "temp_preview." + new URL(this.img.src).pathname.split(".")[1];
|
|
const body = new FormData();
|
|
body.append("image", new File([blob], name));
|
|
body.append("overwrite", "true");
|
|
body.append("type", "temp");
|
|
|
|
const resp = await api.fetchApi("/upload/image", {
|
|
method: "POST",
|
|
body,
|
|
});
|
|
|
|
if (resp.status !== 200) {
|
|
console.error(resp);
|
|
alert(`Error saving preview (${req.status}) ${req.statusText}`);
|
|
return;
|
|
}
|
|
|
|
|
|
await api.fetchApi("/pysssss/save/" + encodeURIComponent(`${this.type}/${this.name}`), {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
filename: name,
|
|
type: "temp",
|
|
}),
|
|
headers: {
|
|
"content-type": "application/json",
|
|
},
|
|
});
|
|
app.refreshComboInNodes();
|
|
},
|
|
});
|
|
|
|
$el("button", {
|
|
textContent: "Show metadata",
|
|
parent: this.imgWrapper,
|
|
onclick: async () => {
|
|
if (preview.meta && Object.keys(preview.meta).length) {
|
|
new MetadataDialog().show(preview.meta);
|
|
} else {
|
|
alert("No image metadata found");
|
|
}
|
|
},
|
|
});
|
|
|
|
const addNavButton = (icon, direction) => {
|
|
$el("button.pysssss-preview-nav", {
|
|
textContent: icon,
|
|
parent: this.imgWrapper,
|
|
onclick: async () => {
|
|
previewIndex += direction;
|
|
if (previewIndex < 0) {
|
|
previewIndex = previews.length - 1;
|
|
} else if (previewIndex >= previews.length) {
|
|
previewIndex = 0;
|
|
}
|
|
updatePreview();
|
|
},
|
|
});
|
|
};
|
|
|
|
if (previews.length > 1) {
|
|
addNavButton("‹", -1);
|
|
addNavButton("›", 1);
|
|
}
|
|
} else if (info.images?.length) {
|
|
$el("span", { style: { opacity: 0.6 }, textContent: "⚠️ All images hidden due to NSFW level setting.", parent: this.imgWrapper });
|
|
}
|
|
|
|
return info;
|
|
})
|
|
.catch((err) => {
|
|
content.textContent = "⚠️ " + err.message;
|
|
});
|
|
}
|
|
}
|
|
|