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", "
"); 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:``, 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:``, 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; } }