JasonSmithSO's picture
Upload 578 files
8866644 verified
import { $el, ComfyDialog } from "../../../../scripts/ui.js";
import { api } from "../../../../scripts/api.js";
import {formatTime} from './utils.js';
import {$t} from "./i18n.js";
import {toast} from "./toast.js";
class MetadataDialog extends ComfyDialog {
constructor() {
super();
this.element.classList.add("easyuse-model-metadata");
}
show(metadata) {
super.show(
$el(
"div",
Object.keys(metadata).map((k) =>
$el("div", [$el("label", { textContent: k }), $el("span", { textContent: metadata[k] })])
)
)
);
}
}
export class ModelInfoDialog extends ComfyDialog {
constructor(name) {
super();
this.name = name;
this.element.classList.add("easyuse-model-info");
}
get customNotes() {
return this.metadata["easyuse.notes"];
}
set customNotes(v) {
this.metadata["easyuse.notes"] = v;
}
get hash() {
return this.metadata["easyuse.sha256"];
}
async show(type, value) {
this.type = type;
const req = api.fetchApi("/easyuse/metadata/" + encodeURIComponent(`${type}/${value}`));
this.info = $el("div", { style: { flex: "auto" } });
// this.img = $el("img", { style: { display: "none" } });
this.imgCurrent = 0
this.imgList = $el("div.easyuse-preview-list",{
style: { display: "none" }
})
this.imgWrapper = $el("div.easyuse-preview", [
$el("div.easyuse-preview-group",[
this.imgList
]),
]);
this.main = $el("main", { style: { display: "flex" } }, [this.imgWrapper, this.info]);
this.content = $el("div.easyuse-model-content", [
$el("div.easyuse-model-header",[$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;
}
parseNote() {
if (!this.customNotes) return [];
let notes = [];
// Extract links from 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;
}
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) => {
this.imgWrapper.style.display = 'block'
// 变更标题信息
let header = this.element.querySelector('.easyuse-model-header')
if(header){
header.replaceChildren(
$el("h2", { textContent: this.name }),
$el("div.easyuse-model-header-remark",[
$el("h5", { textContent: $t("Updated At:") + formatTime(new Date(info.updatedAt),'yyyy/MM/dd')}),
$el("h5", { textContent: $t("Created At:") + formatTime(new Date(info.updatedAt),'yyyy/MM/dd')}),
])
)
}
// 替换内容
let textarea = null
let notes = this.parseNote.call(this)
let editText = $t("✏️ Edit")
console.log(notes)
let textarea_div = $el("div.easyuse-model-detail-textarea",[
$el("p",notes?.length>0 ? notes : {textContent:$t('No notes')}),
])
if(!notes || notes.length == 0) textarea_div.classList.add('empty')
else textarea_div.classList.remove('empty')
this.info.replaceChildren(
$el("div.easyuse-model-detail",[
$el("div.easyuse-model-detail-head.flex-b",[
$el('span',$t("Notes")),
$el("a", {
textContent: editText,
href: "#",
style: {
fontSize: "12px",
float: "right",
color: "var(--warning-color)",
textDecoration: "none",
},
onclick: async (e) => {
e.preventDefault();
if (textarea) {
if(textarea.value != this.customNotes){
toast.showLoading($t('Saving Notes...'))
this.customNotes = textarea.value;
const resp = await api.fetchApi(
"/easyuse/metadata/notes/" + encodeURIComponent(`${this.type}/${this.name}`),
{
method: "POST",
body: this.customNotes,
}
);
toast.hideLoading()
if (resp.status !== 200) {
toast.error($t('Saving Failed'))
console.error(resp);
alert(`Error saving notes (${resp.status}) ${resp.statusText}`);
return;
}
toast.success($t('Saving Succeed'))
notes = this.parseNote.call(this)
console.log(notes)
textarea_div.replaceChildren($el("p",notes?.length>0 ? notes : {textContent:$t('No notes')}));
if(textarea.value) textarea_div.classList.remove('empty')
else textarea_div.classList.add('empty')
}else {
textarea_div.replaceChildren($el("p",{textContent:$t('No notes')}));
textarea_div.classList.add('empty')
}
e.target.textContent = editText;
textarea.remove();
textarea = null;
} else {
e.target.textContent = "💾 Save";
textarea = $el("textarea", {
placeholder: $t("Type your notes here"),
style: {
width: "100%",
minWidth: "200px",
minHeight: "50px",
height:"100px"
},
textContent: this.customNotes,
});
textarea_div.replaceChildren(textarea);
textarea.focus()
}
}
})
]),
textarea_div
]),
$el("div.easyuse-model-detail",[
$el("div.easyuse-model-detail-head",{textContent:$t("Details")}),
$el("div.easyuse-model-detail-body",[
$el("div.easyuse-model-detail-item",[
$el("div.easyuse-model-detail-item-label",{textContent:$t("Type")}),
$el("div.easyuse-model-detail-item-value",{textContent:info.model.type}),
]),
$el("div.easyuse-model-detail-item",[
$el("div.easyuse-model-detail-item-label",{textContent:$t("BaseModel")}),
$el("div.easyuse-model-detail-item-value",{textContent:info.baseModel}),
]),
$el("div.easyuse-model-detail-item",[
$el("div.easyuse-model-detail-item-label",{textContent:$t("Download")}),
$el("div.easyuse-model-detail-item-value",{textContent:info.stats?.downloadCount || 0}),
]),
$el("div.easyuse-model-detail-item",[
$el("div.easyuse-model-detail-item-label",{textContent:$t("Trained Words")}),
$el("div.easyuse-model-detail-item-value",{textContent:info?.trainedWords.join(',') || '-'}),
]),
$el("div.easyuse-model-detail-item",[
$el("div.easyuse-model-detail-item-label",{textContent:$t("Source")}),
$el("div.easyuse-model-detail-item-value",[
$el("label", [
$el("img", {
style: {
width: "14px",
position: "relative",
top: "3px",
margin: "0 5px 0 0",
},
src: "https://civitai.com/favicon.ico",
}),
$el("a", {
href: "https://civitai.com/models/" + info.modelId,
textContent: "View " + info.model.name,
target: "_blank",
})
])
]),
])
]),
])
);
if (info.images?.length) {
this.imgCurrent = 0
this.isSaving = false
info.images.map(cate=>
cate.url &&
this.imgList.appendChild(
$el('div.easyuse-preview-slide',[
$el('div.easyuse-preview-slide-content',[
$el('img',{src:(cate.url)}),
$el("div.save", {
textContent: "Save as preview",
onclick: async () => {
if(this.isSaving) return
this.isSaving = true
toast.showLoading($t('Saving Preview...'))
// Convert the preview to a blob
const blob = await (await fetch(cate.url)).blob();
// Store it in temp
const name = "temp_preview." + new URL(cate.url).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) {
this.isSaving = false
toast.error($t('Saving Failed'))
toast.hideLoading()
console.error(resp);
alert(`Error saving preview (${req.status}) ${req.statusText}`);
return;
}
// Use as preview
await api.fetchApi("/easyuse/save/" + encodeURIComponent(`${this.type}/${this.name}`), {
method: "POST",
body: JSON.stringify({
filename: name,
type: "temp",
}),
headers: {
"content-type": "application/json",
},
}).then(_=>{
toast.success($t('Saving Succeed'))
toast.hideLoading()
});
this.isSaving = false
app.refreshComboInNodes();
},
})
])
])
)
)
let _this = this
this.imgDistance = (-660 * this.imgCurrent).toString()
this.imgList.style.display = ''
this.imgList.style.transform = 'translate3d(' + this.imgDistance +'px, 0px, 0px)'
this.slides = this.imgList.querySelectorAll('.easyuse-preview-slide')
// 添加按钮
this.slideLeftButton = $el("button.left",{
parent: this.imgWrapper,
style:{
display:info.images.length <= 2 ? 'none' : 'block'
},
innerHTML:`<svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16" style="transform: rotate(90deg);"><path d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg>`,
onclick: ()=>{
if(info.images.length <= 2) return
_this.imgList.classList.remove("no-transition")
if(_this.imgCurrent == 0){
_this.imgCurrent = (info.images.length/2)-1
this.slides[this.slides.length-1].style.transform = 'translate3d(' + (-660 * (this.imgCurrent+1)).toString()+'px, 0px, 0px)'
this.slides[this.slides.length-2].style.transform = 'translate3d(' + (-660 * (this.imgCurrent+1)).toString()+'px, 0px, 0px)'
_this.imgList.style.transform = 'translate3d(660px, 0px, 0px)'
setTimeout(_=>{
this.slides[this.slides.length-1].style.transform = 'translate3d(0px, 0px, 0px)'
this.slides[this.slides.length-2].style.transform = 'translate3d(0px, 0px, 0px)'
_this.imgDistance = (-660 * this.imgCurrent).toString()
_this.imgList.style.transform = 'translate3d(' + _this.imgDistance +'px, 0px, 0px)'
_this.imgList.classList.add("no-transition")
},500)
}
else {
_this.imgCurrent = _this.imgCurrent-1
_this.imgDistance = (-660 * this.imgCurrent).toString()
_this.imgList.style.transform = 'translate3d(' + _this.imgDistance +'px, 0px, 0px)'
}
}
})
this.slideRightButton = $el("button.right",{
parent: this.imgWrapper,
style:{
display:info.images.length <= 2 ? 'none' : 'block'
},
innerHTML:`<svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" width="16" height="16" style="transform: rotate(-90deg);"><path d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg>`,
onclick: ()=>{
if(info.images.length <= 2) return
_this.imgList.classList.remove("no-transition")
if( _this.imgCurrent >= (info.images.length/2)-1){
_this.imgCurrent = 0
const max = info.images.length/2
this.slides[0].style.transform = 'translate3d(' + (660 * max).toString()+'px, 0px, 0px)'
this.slides[1].style.transform = 'translate3d(' + (660 * max).toString()+'px, 0px, 0px)'
_this.imgList.style.transform = 'translate3d(' + (-660 * max).toString()+'px, 0px, 0px)'
setTimeout(_=>{
this.slides[0].style.transform = 'translate3d(0px, 0px, 0px)'
this.slides[1].style.transform = 'translate3d(0px, 0px, 0px)'
_this.imgDistance = (-660 * this.imgCurrent).toString()
_this.imgList.style.transform = 'translate3d(' + _this.imgDistance +'px, 0px, 0px)'
_this.imgList.classList.add("no-transition")
},500)
}
else {
_this.imgCurrent = _this.imgCurrent+1
_this.imgDistance = (-660 * this.imgCurrent).toString()
_this.imgList.style.transform = 'translate3d(' + _this.imgDistance +'px, 0px, 0px)'
}
}
})
}
if(info.description){
$el("div", {
parent: this.content,
innerHTML: info.description,
style: {
marginTop: "10px",
},
});
}
return info;
})
.catch((err) => {
this.imgWrapper.style.display = 'none'
content.textContent = "⚠️ " + err.message;
})
.finally(_=>{
})
}
}
export class CheckpointInfoDialog extends ModelInfoDialog {
async addInfo() {
// super.addInfo();
await this.addCivitaiInfo();
}
}
const MAX_TAGS = 500
export class LoraInfoDialog extends ModelInfoDialog {
getTagFrequency() {
if (!this.metadata.ss_tag_frequency) return [];
const datasets = JSON.parse(this.metadata.ss_tag_frequency);
const tags = {};
for (const setName in datasets) {
const set = datasets[setName];
for (const t in set) {
if (t in tags) {
tags[t] += set[t];
} else {
tags[t] = set[t];
}
}
}
return Object.entries(tags).sort((a, b) => b[1] - a[1]);
}
getResolutions() {
let res = [];
if (this.metadata.ss_bucket_info) {
const parsed = JSON.parse(this.metadata.ss_bucket_info);
if (parsed?.buckets) {
for (const { resolution, count } of Object.values(parsed.buckets)) {
res.push([count, `${resolution.join("x")} * ${count}`]);
}
}
}
res = res.sort((a, b) => b[0] - a[0]).map((a) => a[1]);
let r = this.metadata.ss_resolution;
if (r) {
const s = r.split(",");
const w = s[0].replace("(", "");
const h = s[1].replace(")", "");
res.push(`${w.trim()}x${h.trim()} (Base res)`);
} else if ((r = this.metadata["modelspec.resolution"])) {
res.push(r + " (Base res");
}
if (!res.length) {
res.push("⚠️ Unknown");
}
return res;
}
getTagList(tags) {
return tags.map((t) =>
$el(
"li.easyuse-model-tag",
{
dataset: {
tag: t[0],
},
$: (el) => {
el.onclick = () => {
el.classList.toggle("easyuse-model-tag--selected");
};
},
},
[
$el("p", {
textContent: t[0],
}),
$el("span", {
textContent: t[1],
}),
]
)
);
}
addTags() {
let tags = this.getTagFrequency();
let hasMore;
if (tags?.length) {
const c = tags.length;
let list;
if (c > MAX_TAGS) {
tags = tags.slice(0, MAX_TAGS);
hasMore = $el("p", [
$el("span", { textContent: `⚠️ Only showing first ${MAX_TAGS} tags ` }),
$el("a", {
href: "#",
textContent: `Show all ${c}`,
onclick: () => {
list.replaceChildren(...this.getTagList(this.getTagFrequency()));
hasMore.remove();
},
}),
]);
}
list = $el("ol.easyuse-model-tags-list", this.getTagList(tags));
this.tags = $el("div", [list]);
} else {
this.tags = $el("p", { textContent: "⚠️ No tag frequency metadata found" });
}
this.content.append(this.tags);
if (hasMore) {
this.content.append(hasMore);
}
}
async addInfo() {
// this.addInfoEntry("Name", this.metadata.ss_output_name || "⚠️ Unknown");
// this.addInfoEntry("Base Model", this.metadata.ss_sd_model_name || "⚠️ Unknown");
// this.addInfoEntry("Clip Skip", this.metadata.ss_clip_skip || "⚠️ Unknown");
//
// this.addInfoEntry(
// "Resolution",
// $el(
// "select",
// this.getResolutions().map((r) => $el("option", { textContent: r }))
// )
// );
// super.addInfo();
const p = this.addCivitaiInfo();
this.addTags();
const info = await p;
if (info) {
// $el(
// "p",
// {
// parent: this.content,
// textContent: "Trained Words: ",
// },
// [
// $el("pre", {
// textContent: info.trainedWords.join(", "),
// style: {
// whiteSpace: "pre-wrap",
// margin: "10px 0",
// background: "#222",
// padding: "5px",
// borderRadius: "5px",
// maxHeight: "250px",
// overflow: "auto",
// },
// }),
// ]
// );
$el("div", {
parent: this.content,
innerHTML: info.description,
style: {
maxHeight: "250px",
overflow: "auto",
},
});
}
}
createButtons() {
const btns = super.createButtons();
function copyTags(e, tags) {
const textarea = $el("textarea", {
parent: document.body,
style: {
position: "fixed",
},
textContent: tags.map((el) => el.dataset.tag).join(", "),
});
textarea.select();
try {
document.execCommand("copy");
if (!e.target.dataset.text) {
e.target.dataset.text = e.target.textContent;
}
e.target.textContent = "Copied " + tags.length + " tags";
setTimeout(() => {
e.target.textContent = e.target.dataset.text;
}, 1000);
} catch (ex) {
prompt("Copy to clipboard: Ctrl+C, Enter", text);
} finally {
document.body.removeChild(textarea);
}
}
btns.unshift(
$el("button", {
type: "button",
textContent: "Copy Selected",
onclick: (e) => {
copyTags(e, [...this.tags.querySelectorAll(".easyuse-model-tag--selected")]);
},
}),
$el("button", {
type: "button",
textContent: "Copy All",
onclick: (e) => {
copyTags(e, [...this.tags.querySelectorAll(".easyuse-model-tag")]);
},
})
);
return btns;
}
}