Spaces:
Runtime error
Runtime error
| import {RgthreeDialog, RgthreeDialogOptions} from "rgthree/common/dialog.js"; | |
| import { | |
| createElement as $el, | |
| empty, | |
| appendChildren, | |
| getClosestOrSelf, | |
| queryOne, | |
| query, | |
| setAttributes, | |
| } from "rgthree/common/utils_dom.js"; | |
| import { | |
| logoCivitai, | |
| link, | |
| pencilColored, | |
| diskColored, | |
| dotdotdot, | |
| } from "rgthree/common/media/svgs.js"; | |
| import {RgthreeModelInfo} from "typings/rgthree.js"; | |
| import {LORA_INFO_SERVICE} from "rgthree/common/model_info_service.js"; | |
| import {rgthree} from "./rgthree.js"; | |
| import {MenuButton} from "rgthree/common/menu.js"; | |
| import {generateId, injectCss} from "rgthree/common/shared_utils.js"; | |
| import {rgthreeApi} from "rgthree/common/rgthree_api.js"; | |
| /** | |
| * A dialog that displays information about a model/lora/etc. | |
| */ | |
| abstract class RgthreeInfoDialog extends RgthreeDialog { | |
| private modifiedModelData = false; | |
| private modelInfo: RgthreeModelInfo | null = null; | |
| constructor(file: string, type: string = "lora") { | |
| const dialogOptions: RgthreeDialogOptions = { | |
| class: "rgthree-info-dialog", | |
| title: `<h2>Loading...</h2>`, | |
| content: "<center>Loading..</center>", | |
| onBeforeClose: () => { | |
| return true; | |
| }, | |
| }; | |
| super(dialogOptions); | |
| this.init(file); | |
| } | |
| abstract getModelInfo(file: string): Promise<RgthreeModelInfo | null>; | |
| abstract refreshModelInfo(file: string): Promise<RgthreeModelInfo | null>; | |
| abstract clearModelInfo(file: string): Promise<RgthreeModelInfo | null>; | |
| private async init(file: string) { | |
| const cssPromise = injectCss("rgthree/common/css/dialog_model_info.css"); | |
| this.modelInfo = await this.getModelInfo(file); | |
| await cssPromise; | |
| this.setContent(this.getInfoContent()); | |
| this.setTitle(this.modelInfo?.["name"] || this.modelInfo?.["file"] || "Unknown"); | |
| this.attachEvents(); | |
| } | |
| protected override getCloseEventDetail(): {detail: any} { | |
| const detail = { | |
| dirty: this.modifiedModelData, | |
| }; | |
| return {detail}; | |
| } | |
| private attachEvents() { | |
| this.contentElement.addEventListener("click", async (e: MouseEvent) => { | |
| const target = getClosestOrSelf(e.target as HTMLElement, "[data-action]"); | |
| const action = target?.getAttribute("data-action"); | |
| if (!target || !action) { | |
| return; | |
| } | |
| await this.handleEventAction(action, target, e); | |
| }); | |
| } | |
| private async handleEventAction(action: string, target: HTMLElement, e?: Event) { | |
| const info = this.modelInfo!; | |
| if (!info?.file) { | |
| return; | |
| } | |
| if (action === "fetch-civitai") { | |
| this.modelInfo = await this.refreshModelInfo(info.file); | |
| this.setContent(this.getInfoContent()); | |
| this.setTitle(this.modelInfo?.["name"] || this.modelInfo?.["file"] || "Unknown"); | |
| } else if (action === "copy-trained-words") { | |
| const selected = query(".-rgthree-is-selected", target.closest("tr")!); | |
| const text = selected.map((el) => el.getAttribute("data-word")).join(", "); | |
| await navigator.clipboard.writeText(text); | |
| rgthree.showMessage({ | |
| id: "copy-trained-words-" + generateId(4), | |
| type: "success", | |
| message: `Successfully copied ${selected.length} key word${ | |
| selected.length === 1 ? "" : "s" | |
| }.`, | |
| timeout: 4000, | |
| }); | |
| } else if (action === "toggle-trained-word") { | |
| target?.classList.toggle("-rgthree-is-selected"); | |
| const tr = target.closest("tr"); | |
| if (tr) { | |
| const span = queryOne("td:first-child > *", tr)!; | |
| let small = queryOne("small", span); | |
| if (!small) { | |
| small = $el("small", {parent: span}); | |
| } | |
| const num = query(".-rgthree-is-selected", tr).length; | |
| small.innerHTML = num | |
| ? `${num} selected | <span role="button" data-action="copy-trained-words">Copy</span>` | |
| : ""; | |
| // this.handleEventAction('copy-trained-words', target, e); | |
| } | |
| } else if (action === "edit-row") { | |
| const tr = target!.closest("tr")!; | |
| const td = queryOne("td:nth-child(2)", tr)!; | |
| const input = td.querySelector("input,textarea"); | |
| if (!input) { | |
| const fieldName = tr.dataset["fieldName"] as string; | |
| tr.classList.add("-rgthree-editing"); | |
| const isTextarea = fieldName === "userNote"; | |
| const input = $el(`${isTextarea ? "textarea" : 'input[type="text"]'}`, { | |
| value: td.textContent, | |
| }); | |
| input.addEventListener("keydown", (e) => { | |
| if (!isTextarea && e.key === "Enter") { | |
| const modified = saveEditableRow(info!, tr, true); | |
| this.modifiedModelData = this.modifiedModelData || modified; | |
| e.stopPropagation(); | |
| e.preventDefault(); | |
| } else if (e.key === "Escape") { | |
| const modified = saveEditableRow(info!, tr, false); | |
| this.modifiedModelData = this.modifiedModelData || modified; | |
| e.stopPropagation(); | |
| e.preventDefault(); | |
| } | |
| }); | |
| appendChildren(empty(td), [input]); | |
| input.focus(); | |
| } else if (target!.nodeName.toLowerCase() === "button") { | |
| const modified = saveEditableRow(info!, tr, true); | |
| this.modifiedModelData = this.modifiedModelData || modified; | |
| } | |
| e?.preventDefault(); | |
| e?.stopPropagation(); | |
| } | |
| } | |
| private getInfoContent() { | |
| const info = this.modelInfo || {}; | |
| const civitaiLink = info.links?.find((i) => i.includes("civitai.com/models")); | |
| const html = ` | |
| <ul class="rgthree-info-area"> | |
| <li title="Type" class="rgthree-info-tag -type -type-${( | |
| info.type || "" | |
| ).toLowerCase()}"><span>${info.type || ""}</span></li> | |
| <li title="Base Model" class="rgthree-info-tag -basemodel -basemodel-${( | |
| info.baseModel || "" | |
| ).toLowerCase()}"><span>${info.baseModel || ""}</span></li> | |
| <li class="rgthree-info-menu" stub="menu"></li> | |
| ${ | |
| "" | |
| // !civitaiLink | |
| // ? "" | |
| // : ` | |
| // <li title="Visit on Civitai" class="-link -civitai"><a href="${civitaiLink}" target="_blank">Civitai ${link}</a></li> | |
| // ` | |
| } | |
| </ul> | |
| <table class="rgthree-info-table"> | |
| ${infoTableRow("File", info.file || "")} | |
| ${infoTableRow("Hash (sha256)", info.sha256 || "")} | |
| ${ | |
| civitaiLink | |
| ? infoTableRow( | |
| "Civitai", | |
| `<a href="${civitaiLink}" target="_blank">${logoCivitai}View on Civitai</a>`, | |
| ) | |
| : info.raw?.civitai?.error === "Model not found" | |
| ? infoTableRow( | |
| "Civitai", | |
| '<i>Model not found</i> <span class="-help" title="The model was not found on civitai with the sha256 hash. It\'s possible the model was removed, re-uploaded, or was never on civitai to begin with."></span>', | |
| ) | |
| : info.raw?.civitai?.error | |
| ? infoTableRow("Civitai", info.raw?.civitai?.error) | |
| : !info.raw?.civitai | |
| ? infoTableRow( | |
| "Civitai", | |
| `<button class="rgthree-button" data-action="fetch-civitai">Fetch info from civitai</button>`, | |
| ) | |
| : "" | |
| } | |
| ${infoTableRow( | |
| "Name", | |
| info.name || info.raw?.metadata?.ss_output_name || "", | |
| "The name for display.", | |
| "name", | |
| )} | |
| ${ | |
| !info.baseModelFile && !info.baseModelFile | |
| ? "" | |
| : infoTableRow( | |
| "Base Model", | |
| (info.baseModel || "") + (info.baseModelFile ? ` (${info.baseModelFile})` : ""), | |
| ) | |
| } | |
| ${ | |
| !info.trainedWords?.length | |
| ? "" | |
| : infoTableRow( | |
| "Trained Words", | |
| getTrainedWordsMarkup(info.trainedWords) ?? "", | |
| "Trained words from the metadata and/or civitai. Click to select for copy.", | |
| ) | |
| } | |
| ${ | |
| !info.raw?.metadata?.ss_clip_skip || info.raw?.metadata?.ss_clip_skip == "None" | |
| ? "" | |
| : infoTableRow("Clip Skip", info.raw?.metadata?.ss_clip_skip) | |
| } | |
| ${infoTableRow( | |
| "Strength Min", | |
| info.strengthMin ?? "", | |
| "The recommended minimum strength, In the Power Lora Loader node, strength will signal when it is below this threshold.", | |
| "strengthMin", | |
| )} | |
| ${infoTableRow( | |
| "Strength Max", | |
| info.strengthMax ?? "", | |
| "The recommended maximum strength. In the Power Lora Loader node, strength will signal when it is above this threshold.", | |
| "strengthMax", | |
| )} | |
| ${ | |
| "" /*infoTableRow( | |
| "User Tags", | |
| info.userTags?.join(", ") ?? "", | |
| "A list of tags to make filtering easier in the Power Lora Chooser.", | |
| "userTags", | |
| )*/ | |
| } | |
| ${infoTableRow( | |
| "Additional Notes", | |
| info.userNote ?? "", | |
| "Additional notes you'd like to keep and reference in the info dialog.", | |
| "userNote", | |
| )} | |
| </table> | |
| <ul class="rgthree-info-images">${ | |
| info.images | |
| ?.map( | |
| (img) => ` | |
| <li> | |
| <figure> | |
| <img src="${img.url}" /> | |
| <figcaption><!-- | |
| -->${imgInfoField( | |
| "", | |
| img.civitaiUrl | |
| ? `<a href="${img.civitaiUrl}" target="_blank">civitai${link}</a>` | |
| : undefined, | |
| )}<!-- | |
| -->${imgInfoField("seed", img.seed)}<!-- | |
| -->${imgInfoField("steps", img.steps)}<!-- | |
| -->${imgInfoField("cfg", img.cfg)}<!-- | |
| -->${imgInfoField("sampler", img.sampler)}<!-- | |
| -->${imgInfoField("model", img.model)}<!-- | |
| -->${imgInfoField("positive", img.positive)}<!-- | |
| -->${imgInfoField("negative", img.negative)}<!-- | |
| --><!--${ | |
| "" | |
| // img.resources?.length | |
| // ? ` | |
| // <tr><td>Resources</td><td><ul> | |
| // ${(img.resources || []) | |
| // .map( | |
| // (r) => ` | |
| // <li>[${r.type || ""}] ${r.name || ""} ${ | |
| // r.weight != null ? `@ ${r.weight}` : "" | |
| // }</li> | |
| // `, | |
| // ) | |
| // .join("")} | |
| // </ul></td></tr> | |
| // ` | |
| // : "" | |
| }--></figcaption> | |
| </figure> | |
| </li>`, | |
| ) | |
| .join("") ?? "" | |
| }</ul> | |
| `; | |
| const div = $el("div", {html}); | |
| if (rgthree.isDevMode()) { | |
| setAttributes(queryOne('[stub="menu"]', div)!, { | |
| children: [ | |
| new MenuButton({ | |
| icon: dotdotdot, | |
| options: [ | |
| {label: "More Actions", type: "title"}, | |
| { | |
| label: "Open API JSON", | |
| callback: async (e: PointerEvent) => { | |
| if (this.modelInfo?.file) { | |
| window.open( | |
| `rgthree/api/loras/info?file=${encodeURIComponent(this.modelInfo.file)}`, | |
| ); | |
| } | |
| }, | |
| }, | |
| { | |
| label: "Clear all local info", | |
| callback: async (e: PointerEvent) => { | |
| if (this.modelInfo?.file) { | |
| this.modelInfo = await LORA_INFO_SERVICE.clearFetchedInfo(this.modelInfo.file); | |
| this.setContent(this.getInfoContent()); | |
| this.setTitle( | |
| this.modelInfo?.["name"] || this.modelInfo?.["file"] || "Unknown", | |
| ); | |
| } | |
| }, | |
| }, | |
| ], | |
| }), | |
| ], | |
| }); | |
| } | |
| return div; | |
| } | |
| } | |
| export class RgthreeLoraInfoDialog extends RgthreeInfoDialog { | |
| override async getModelInfo(file: string) { | |
| return LORA_INFO_SERVICE.getInfo(file, false, false); | |
| } | |
| override async refreshModelInfo(file: string) { | |
| return LORA_INFO_SERVICE.refreshInfo(file); | |
| } | |
| override async clearModelInfo(file: string) { | |
| return LORA_INFO_SERVICE.clearFetchedInfo(file); | |
| } | |
| } | |
| /** | |
| * Generates a uniform markup string for a table row. | |
| */ | |
| function infoTableRow( | |
| name: string, | |
| value: string | number, | |
| help: string = "", | |
| editableFieldName = "", | |
| ) { | |
| return ` | |
| <tr class="${editableFieldName ? "editable" : ""}" ${ | |
| editableFieldName ? `data-field-name="${editableFieldName}"` : "" | |
| }> | |
| <td><span>${name} ${help ? `<span class="-help" title="${help}"></span>` : ""}<span></td> | |
| <td ${editableFieldName ? "" : 'colspan="2"'}>${ | |
| String(value).startsWith("<") ? value : `<span>${value}<span>` | |
| }</td> | |
| ${ | |
| editableFieldName | |
| ? `<td style="width: 24px;"><button class="rgthree-button-reset rgthree-button-edit" data-action="edit-row">${pencilColored}${diskColored}</button></td>` | |
| : "" | |
| } | |
| </tr>`; | |
| } | |
| function getTrainedWordsMarkup(words: RgthreeModelInfo["trainedWords"]) { | |
| let markup = `<ul class="rgthree-info-trained-words-list">`; | |
| for (const wordData of words || []) { | |
| markup += `<li title="${wordData.word}" data-word="${ | |
| wordData.word | |
| }" class="rgthree-info-trained-words-list-item" data-action="toggle-trained-word"> | |
| <span>${wordData.word}</span> | |
| ${wordData.civitai ? logoCivitai : ""} | |
| ${wordData.count != null ? `<small>${wordData.count}</small>` : ""} | |
| </li>`; | |
| } | |
| markup += `</ul>`; | |
| return markup; | |
| } | |
| /** | |
| * Saves / cancels an editable row. Returns a boolean if the data was modified. | |
| */ | |
| function saveEditableRow(info: RgthreeModelInfo, tr: HTMLElement, saving = true): boolean { | |
| const fieldName = tr.dataset["fieldName"] as "file"; | |
| const input = queryOne<HTMLInputElement>("input,textarea", tr)!; | |
| let newValue = info[fieldName] ?? ""; | |
| let modified = false; | |
| if (saving) { | |
| newValue = input!.value; | |
| if (fieldName.startsWith("strength")) { | |
| if (Number.isNaN(Number(newValue))) { | |
| alert(`You must enter a number into the ${fieldName} field.`); | |
| return false; | |
| } | |
| newValue = (Math.round(Number(newValue) * 100) / 100).toFixed(2); | |
| } | |
| LORA_INFO_SERVICE.savePartialInfo(info.file!, {[fieldName]: newValue}); | |
| modified = true; | |
| } | |
| tr.classList.remove("-rgthree-editing"); | |
| const td = queryOne("td:nth-child(2)", tr)!; | |
| appendChildren(empty(td), [$el("span", {text: newValue})]); | |
| return modified; | |
| } | |
| function imgInfoField(label: string, value?: string | number) { | |
| return value != null ? `<span>${label ? `<label>${label} </label>` : ""}${value}</span>` : ""; | |
| } | |