1197 lines
35 KiB
JavaScript
1197 lines
35 KiB
JavaScript
// Manages LoRA and embedding model items for the Image Drawer, including fetching from /jnodes_model_items API.
|
|
// Provides model card creation, metadata tooltips, copy-to-prompt functionality, and editing via ModelEditForm.
|
|
|
|
import { api } from "/scripts/api.js";
|
|
import { $el } from "/scripts/ui.js";
|
|
import { utilitiesInstance } from "../../common/Utilities.js"
|
|
|
|
import {
|
|
setting_CopyLoraTextPattern_Default, setting_ModelCardAspectRatio,
|
|
setting_CopyModelTrainedWordsEndCharacter_Default as setting_CopyModelTrainedWordsEndCharacter_Default,
|
|
setting_ModelRules
|
|
} from "../../common/SettingsManager.js";
|
|
|
|
import { ModalManager } from "../../common/ModalManager.js";
|
|
import { ModelEditForm, FormObject, FormInfo } from "../../common/ModelEditForm.js";
|
|
import { findFirstImageDrawerInstanceWithGivenContext } from "../Core/ImageDrawerModule.js";
|
|
|
|
const NoImagePlaceholder = new URL(`../../assets/NoImage.png`, import.meta.url);
|
|
|
|
let cachedLorasObject = undefined;
|
|
let cachedEmbeddingsObject = undefined;
|
|
|
|
/**
|
|
* Gets a list of lora names
|
|
* @param {boolean} bForceRefresh
|
|
* @param {string} subdirectory - Optional subdirectory filter (e.g. "style")
|
|
* @returns An array of script urls to import
|
|
*/
|
|
export async function getLoras(bForceRefresh = false, subdirectory = "", signal = undefined) {
|
|
if (!subdirectory && (bForceRefresh || !cachedLorasObject)) {
|
|
const resp = await api.fetchApi("/jnodes_model_items?type=loras", { cache: "no-store", signal });
|
|
const asJson = await resp.json();
|
|
cachedLorasObject = asJson;
|
|
return asJson;
|
|
}
|
|
|
|
if (subdirectory) {
|
|
const resp = await api.fetchApi(`/jnodes_model_items?type=loras&subdirectory=${encodeURIComponent(subdirectory)}`, { cache: "no-store", signal });
|
|
return await resp.json();
|
|
}
|
|
|
|
return cachedLorasObject;
|
|
}
|
|
|
|
/**
|
|
* Gets a list of embedding names
|
|
* @param {boolean} bForceRefresh
|
|
* @param {string} subdirectory - Optional subdirectory filter (e.g. "style")
|
|
* @returns An array of script urls to import
|
|
*/
|
|
export async function getEmbeddings(bForceRefresh = false, subdirectory = "", signal = undefined) {
|
|
if (!subdirectory && (bForceRefresh || !cachedEmbeddingsObject)) {
|
|
const resp = await api.fetchApi("/jnodes_model_items?type=embeddings", { cache: "no-store", signal });
|
|
const asJson = await resp.json();
|
|
cachedEmbeddingsObject = asJson;
|
|
return asJson;
|
|
}
|
|
|
|
if (subdirectory) {
|
|
const resp = await api.fetchApi(`/jnodes_model_items?type=embeddings&subdirectory=${encodeURIComponent(subdirectory)}`, { cache: "no-store", signal });
|
|
return await resp.json();
|
|
}
|
|
|
|
return cachedEmbeddingsObject;
|
|
}
|
|
|
|
function getLoraTextFontSize(emMultiplier) {
|
|
const out = `${emMultiplier}em`;
|
|
//console.log(out);
|
|
return out;
|
|
}
|
|
|
|
export async function createExtraNetworkCard(nameText, familiars, type, imageDrawerInstance) {
|
|
|
|
if (!nameText) {
|
|
return;
|
|
}
|
|
|
|
// console.log(familiars);
|
|
|
|
const bIsLora = type == "loras";
|
|
|
|
const modelElement =
|
|
$el("div.extraNetworksCard", {
|
|
id: "extra-networks-card",
|
|
style: {
|
|
textAlign: "center",
|
|
objectFit: "var(--div-fit, contain)",
|
|
borderRadius: "4px",
|
|
position: "relative",
|
|
aspectRatio: setting_ModelCardAspectRatio.value,
|
|
},
|
|
});
|
|
|
|
modelElement.fileInfo = {};
|
|
|
|
let cardInfo = {
|
|
nameText: nameText,
|
|
trainedWords: "",
|
|
tags: [],
|
|
modelId: undefined, // The base model ID for civit.ai. Not specific to any version of the model.
|
|
modelVersionId: undefined, // The model version ID, specific to a certain version of the model.
|
|
lastViewedImageIndex: 0,
|
|
}
|
|
|
|
let userCardInfoOverrides = {
|
|
friendlyName: "",
|
|
userTags: "",
|
|
overrideWithUserTags: false,
|
|
userTrainedWords: "",
|
|
overrideWithUserTrainedWords: false,
|
|
preferredDefaultPreviewIndex: 1,
|
|
defaultWeightModel: 1.0,
|
|
defaultWeightClip: 1.0,
|
|
loraTextPatternOverride: "",
|
|
linkOverride: "",
|
|
}
|
|
|
|
function getModelRules() {
|
|
|
|
if (setting_ModelRules.value?.length > 0) {
|
|
|
|
for (const rules of setting_ModelRules.value) {
|
|
|
|
if (rules.MatchingDirectoryRegex) {
|
|
|
|
try {
|
|
const regex = utilitiesInstance.parseRegexFromInputWidget(rules.MatchingDirectoryRegex);
|
|
if (regex?.test(familiars.full_name)) {
|
|
return rules;
|
|
}
|
|
} catch (e) {
|
|
console.warn("Invalid regex in model rule:", rules.MatchingDirectoryRegex, e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function getCopyLoraTextPattern() {
|
|
|
|
const rules = getModelRules();
|
|
|
|
if (rules?.CopyLoraTextPattern) {
|
|
|
|
return rules.CopyLoraTextPattern;
|
|
}
|
|
|
|
return setting_CopyLoraTextPattern_Default.value;
|
|
}
|
|
|
|
function getUserCardInfoFormObjects() {
|
|
|
|
const getDefaultValueFromData = (key, defaultValue) => {
|
|
|
|
if (userCardInfoOverrides[key]) {
|
|
|
|
return userCardInfoOverrides[key];
|
|
}
|
|
|
|
return defaultValue;
|
|
}
|
|
|
|
const formObjects = [
|
|
new FormObject(
|
|
"friendlyName", "text", "",
|
|
"",
|
|
"The name to display in the card"
|
|
),
|
|
new FormObject(
|
|
"userTags", "text", "",
|
|
"",
|
|
"Tags that aid with search and organization, separated by comma."
|
|
),
|
|
new FormObject(
|
|
"overrideWithUserTags", "checkbox", false,
|
|
"",
|
|
"Whether to replace the tags from Civit.ai with entered user tags (true) or add to them (false)"
|
|
),
|
|
new FormObject(
|
|
"userTrainedWords", "text", "",
|
|
"",
|
|
"Words or whole prompts that trigger the model to work"
|
|
),
|
|
new FormObject(
|
|
"overrideWithUserTrainedWords", "checkbox", false,
|
|
"",
|
|
"Whether to replace the trained words from Civit.ai with entered user trained words (true) or add to them (false)"
|
|
),
|
|
new FormObject(
|
|
"preferredDefaultPreviewIndex", "number", 1,
|
|
"",
|
|
"The index of the preview image or video you want to show on the model card by default. Values lower than 1 or higher than the number of total previews will be clamped",
|
|
{min: 1, max: familiars.familiar_images.length}
|
|
),
|
|
new FormObject(
|
|
"defaultWeightModel", "number", 1.0,
|
|
"",
|
|
"The weight used for the model when copying a lora as text",
|
|
{step: 0.01}
|
|
),
|
|
new FormObject(
|
|
"defaultWeightClip", "number", 1.0,
|
|
"",
|
|
"The weight used for the clip when copying a lora as text",
|
|
{step: 0.01}
|
|
),
|
|
new FormObject(
|
|
"loraTextPatternOverride", "text", "",
|
|
"",
|
|
"Define the way text loras are constructed when copied to the clipboard for this specific lora. DOES NOT APPLY TO OTHER MODEL TYPES. " +
|
|
"put variable names in double braces '{{}}'. Variables are 'modelName', 'strengthModel', and 'strengthClip'. " +
|
|
`For example, '${setting_CopyLoraTextPattern_Default.getDefaultValue()}'`,
|
|
{},
|
|
(inputWidget) => {
|
|
inputWidget.value = getCopyLoraTextPattern() || "<lora:{{modelName}}:{{strengthModel}}:{{strengthClip}}>";;
|
|
}
|
|
),
|
|
new FormObject(
|
|
"linkOverride", "text", "",
|
|
"",
|
|
"A hyperlink to the resource, if it was downloaded. Leave blank to construct a link from other info sources automatically."
|
|
),
|
|
];
|
|
|
|
for (let formObject of formObjects) {
|
|
formObject._defaultValue =
|
|
getDefaultValueFromData(formObject._name, formObject._defaultValue);
|
|
}
|
|
|
|
return formObjects;
|
|
}
|
|
|
|
function getNameToUse() {
|
|
|
|
if (userCardInfoOverrides.friendlyName) {
|
|
return `${userCardInfoOverrides.friendlyName} (${cardInfo.lora_meta_name ? cardInfo.lora_meta_name : cardInfo.nameText})`;
|
|
} else if (cardInfo.lora_meta_name) {
|
|
return cardInfo.lora_meta_name;
|
|
} else if (cardInfo.civit_model_name) {
|
|
return cardInfo.civit_model_name;
|
|
}
|
|
|
|
return cardInfo.nameText;
|
|
}
|
|
|
|
function getTags() {
|
|
|
|
function commaSeparatedStringToTags(inString) {
|
|
const split = inString.split(",");
|
|
|
|
return split.map(element => element.trim());
|
|
}
|
|
|
|
if (userCardInfoOverrides.userTags) {
|
|
const asArray = commaSeparatedStringToTags(userCardInfoOverrides.userTags);
|
|
if (userCardInfoOverrides.overrideWithUserTags) {
|
|
return asArray;
|
|
} else if (cardInfo.tags) {
|
|
return [...new Set(cardInfo.tags.concat(asArray))];
|
|
}
|
|
} else if (cardInfo.tags) {
|
|
return cardInfo.tags;
|
|
}
|
|
}
|
|
|
|
function getTrainedWords() {
|
|
|
|
let trainedWords =
|
|
userCardInfoOverrides.overrideWithUserTrainedWords ?
|
|
userCardInfoOverrides.userTrainedWords :
|
|
cardInfo.trainedWords + userCardInfoOverrides.userTrainedWords;
|
|
|
|
if (trainedWords) {
|
|
trainedWords = trainedWords.trim()
|
|
|
|
function getEndCharacter() {
|
|
|
|
const rules = getModelRules();
|
|
|
|
if (rules?.CopyModelTrainedWordsEndCharacter) {
|
|
|
|
return rules.CopyModelTrainedWordsEndCharacter;
|
|
}
|
|
|
|
return setting_CopyModelTrainedWordsEndCharacter_Default.value;
|
|
}
|
|
|
|
const endCharacter = getEndCharacter();
|
|
|
|
if (!trainedWords.endsWith(endCharacter.trim())) {
|
|
trainedWords += endCharacter;
|
|
}
|
|
trainedWords = trainedWords.replace(/\\\(/g, "(").replace(/\\\)/g, ")").replace(/\\"/g, '"');
|
|
}
|
|
|
|
return trainedWords;
|
|
}
|
|
|
|
function getModelLink() {
|
|
|
|
if (userCardInfoOverrides.linkOverride) {
|
|
return userCardInfoOverrides.linkOverride;
|
|
}
|
|
|
|
let href = `https://civitai.com/models/${cardInfo.modelId}`;
|
|
if (cardInfo.modelVersionId) {
|
|
href = href + `?modelVersionId=${cardInfo.modelVersionId}`;
|
|
}
|
|
|
|
return href;
|
|
}
|
|
|
|
// Load the first image as the object cover image.
|
|
// If one does not exist, fall back to placeholder image.
|
|
function getHrefForFamiliarImage(index) {
|
|
if (familiars?.familiar_images?.length > 0 && familiars.familiar_images[index]) {
|
|
|
|
if (familiars.familiar_images[index].hasOwnProperty("file_name")) {
|
|
|
|
const file_name = familiars.familiar_images[index].file_name;
|
|
return `/jnodes_view_image?filename=${encodeURIComponent(file_name)}&type=${type}`;
|
|
}
|
|
}
|
|
return NoImagePlaceholder;
|
|
}
|
|
|
|
async function parseUserCardInfo(infoString) {
|
|
try {
|
|
const loadedJson = JSON.parse(infoString);
|
|
|
|
if (loadedJson) {
|
|
for (const key in userCardInfoOverrides) {
|
|
if (key in loadedJson) {
|
|
userCardInfoOverrides[key] = loadedJson[key];
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
console.log(`Could nat parse user card info from text, are you adding a trailing comma to the last member of the json? ${infoString}`);
|
|
}
|
|
}
|
|
|
|
async function parseFamiliarInfos() {
|
|
let infoMap = {};
|
|
if (familiars?.metadata) {
|
|
infoMap["lora_meta"] = familiars.metadata;
|
|
}
|
|
if (familiars?.familiar_infos?.length > 0) {
|
|
for (const familiar_info of familiars.familiar_infos) {
|
|
try {
|
|
|
|
if (familiar_info.file_name.includes(".user.info")) {
|
|
parseUserCardInfo(familiar_info.loaded_text);
|
|
continue;
|
|
}
|
|
|
|
const bIsFullInfo = familiar_info.file_name.includes("civit.full.info");
|
|
let loadedJson = JSON.parse(familiar_info.loaded_text);
|
|
if (loadedJson) {
|
|
let jsonKeys = Object.keys(loadedJson);
|
|
|
|
for (const key of jsonKeys) {
|
|
|
|
let keyTouse = key;
|
|
|
|
if (key == "id") {
|
|
if (bIsFullInfo) {
|
|
keyTouse = "modelId";
|
|
} else {
|
|
keyTouse = "modelVersionId";
|
|
}
|
|
}
|
|
infoMap[keyTouse] = loadedJson[key];
|
|
}
|
|
}
|
|
} catch (jsonError) {
|
|
|
|
// pass
|
|
}
|
|
}
|
|
}
|
|
|
|
// Prefer user set friendlyName over meta or data from civit.ai
|
|
if (infoMap.lora_meta) {
|
|
if (infoMap.lora_meta?.modelspec?.title
|
|
&& infoMap.lora_meta.modelspec.title.trim() != ""
|
|
&& infoMap.lora_meta.modelspec.title.trim() != cardInfo.nameText.trim()
|
|
) {
|
|
cardInfo.lora_meta_name = `${infoMap.lora_meta.modelspec.title} (${cardInfo.nameText})`;
|
|
} else if (infoMap.ss_output_name
|
|
&& infoMap.ss_output_name.trim() != ""
|
|
&& infoMap.ss_output_name.trim() != cardInfo.nameText.trim()
|
|
) {
|
|
cardInfo.lora_meta_name = `${infoMap.ss_output_name} (${cardInfo.nameText})`;
|
|
}
|
|
} else if (infoMap.model && infoMap.model.name) {
|
|
if (infoMap.model.name.trim() != cardInfo.nameText.trim()) {
|
|
cardInfo.civit_model_name = `${infoMap.model.name} (${cardInfo.nameText})`;
|
|
}
|
|
}
|
|
|
|
// Prefer user set cardInfo.tags over data from civit.ai
|
|
if (infoMap.tags) {
|
|
for (const tag of infoMap.tags) {
|
|
cardInfo.tags.push(tag.trim());
|
|
}
|
|
}
|
|
|
|
// Prefer user set cardInfo.trainedWords over data from civit.ai over meta
|
|
if (infoMap.trainedWords && infoMap.trainedWords.length > 0) {
|
|
|
|
if (Array.isArray(infoMap.trainedWords)) {
|
|
cardInfo.trainedWords = infoMap.trainedWords.join(", ")
|
|
} else {
|
|
cardInfo.trainedWords = infoMap.trainedWords;
|
|
}
|
|
|
|
} else {
|
|
|
|
let tagMap = {};
|
|
|
|
if (infoMap.lora_meta?.ss_tag_frequency) {
|
|
const tagObject = JSON.parse(infoMap.lora_meta.ss_tag_frequency)
|
|
for (const key of Object.keys(tagObject)) {
|
|
for (const tag of Object.keys(tagObject[key])) {
|
|
let trimmedTag = tag.trim();
|
|
if (trimmedTag != "") {
|
|
let frequency = tagObject[key][tag];
|
|
tagMap[trimmedTag] = frequency;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (Object.keys(tagMap).length > 1) {
|
|
Object.entries(tagMap).sort(([keyA, valueA], [keyB, valueB]) => {
|
|
// First, sort by value (numeric)
|
|
if (valueA !== valueB) {
|
|
return valueA - valueB;
|
|
}
|
|
// If values are the same, sort by key (alphabetically)
|
|
return keyA.localeCompare(keyB);
|
|
});
|
|
}
|
|
|
|
for (const tag of Object.keys(tagMap)) {
|
|
|
|
cardInfo.trainedWords += ` ${tag},`;
|
|
}
|
|
}
|
|
|
|
cardInfo.modelVersionId = infoMap.modelVersionId || undefined;
|
|
cardInfo.modelId = infoMap.modelId || cardInfo.modelVersionId || undefined;
|
|
|
|
return infoMap;
|
|
}
|
|
|
|
const infoMap = await parseFamiliarInfos();
|
|
|
|
function createImageSwitchButton(bIsLeft) {
|
|
|
|
if (familiars.familiar_images.length < 2) {
|
|
return;
|
|
}
|
|
|
|
async function updateBackgroundImageContainer() {
|
|
|
|
backgroundImageContainer.backgroundImage.style.display = "none";
|
|
backgroundImageContainer.backgroundVideo.style.display = "none";
|
|
|
|
let newHref = modelElement.fileInfo.imageHref = getHrefForFamiliarImage(backgroundImageContainer.lastViewedImageIndex);
|
|
let bIsVideoPreview = await utilitiesInstance.isHrefVideo(newHref);
|
|
|
|
if (bIsVideoPreview) {
|
|
|
|
backgroundImageContainer.backgroundVideo.style.display = "block";
|
|
backgroundImageContainer.backgroundVideo.src = newHref;
|
|
} else {
|
|
|
|
backgroundImageContainer.backgroundImage.style.display = "block";
|
|
backgroundImageContainer.backgroundImage.src = newHref;
|
|
}
|
|
|
|
updateImageCounterText();
|
|
}
|
|
|
|
function onClickLeft(e) {
|
|
backgroundImageContainer.lastViewedImageIndex -= 1;
|
|
if (backgroundImageContainer.lastViewedImageIndex < 0) {
|
|
backgroundImageContainer.lastViewedImageIndex = familiars.familiar_images.length - 1;
|
|
}
|
|
|
|
updateBackgroundImageContainer();
|
|
}
|
|
|
|
|
|
function onClickRight(e) {
|
|
backgroundImageContainer.lastViewedImageIndex += 1;
|
|
if (backgroundImageContainer.lastViewedImageIndex == familiars.familiar_images.length) {
|
|
backgroundImageContainer.lastViewedImageIndex = 0;
|
|
}
|
|
|
|
updateBackgroundImageContainer();
|
|
}
|
|
|
|
return $el("button", {
|
|
style: {
|
|
position: "absolute",
|
|
top: "50%",
|
|
left: bIsLeft ? "5%" : "85%",
|
|
transformOrigin: "center",
|
|
fontSize: getLoraTextFontSize(1),
|
|
},
|
|
onclick: bIsLeft ? onClickLeft : onClickRight,
|
|
}, [
|
|
$el("label", {
|
|
textContent: bIsLeft ? "<" : ">",
|
|
}),
|
|
]);
|
|
}
|
|
|
|
const startIndex = utilitiesInstance.clamp(
|
|
userCardInfoOverrides.preferredDefaultPreviewIndex ?
|
|
userCardInfoOverrides.preferredDefaultPreviewIndex - 1:
|
|
cardInfo.lastViewedImageIndex,
|
|
0, familiars.familiar_images.length - 1);
|
|
const backgroundImageContainer =
|
|
$el("div", {
|
|
id: "backgroundImageContainer",
|
|
lastViewedImageIndex: startIndex,
|
|
style: {
|
|
width: "100%",
|
|
height: "100%",
|
|
}
|
|
},
|
|
);
|
|
modelElement.fileInfo.imageHref = backgroundImageContainer.dataSrc = getHrefForFamiliarImage(startIndex);
|
|
|
|
backgroundImageContainer.backgroundImage = $el("img");
|
|
backgroundImageContainer.appendChild(backgroundImageContainer.backgroundImage);
|
|
Object.assign(backgroundImageContainer.backgroundImage.style, {
|
|
objectFit: "cover",
|
|
width: "100%",
|
|
height: "100%",
|
|
display: "none"
|
|
});
|
|
|
|
backgroundImageContainer.backgroundVideo = $el("video");
|
|
backgroundImageContainer.backgroundVideo.setAttribute("NoVideoControl", "");
|
|
backgroundImageContainer.appendChild(backgroundImageContainer.backgroundVideo);
|
|
Object.assign(backgroundImageContainer.backgroundVideo.style, {
|
|
objectFit: "cover",
|
|
width: "100%",
|
|
height: "100%",
|
|
display: "none"
|
|
});
|
|
|
|
let imageCounterLabel;
|
|
|
|
function updateImageCounterText() {
|
|
if (imageCounterLabel) {
|
|
if (familiars.familiar_images.length < 1) {
|
|
return;
|
|
}
|
|
|
|
imageCounterLabel.textContent =
|
|
`${backgroundImageContainer.lastViewedImageIndex + 1}/${familiars.familiar_images.length}`
|
|
}
|
|
}
|
|
|
|
const setPreferredDefaultPreviewIndexToCurrent = () => {
|
|
|
|
if (familiars.familiar_images.length < 1) {
|
|
return;
|
|
}
|
|
|
|
userCardInfoOverrides.preferredDefaultPreviewIndex = backgroundImageContainer.lastViewedImageIndex + 1;
|
|
|
|
saveUserModelInfo(JSON.stringify(userCardInfoOverrides));
|
|
};
|
|
|
|
function createImageCounterElement() {
|
|
|
|
imageCounterLabel = $el("label", {
|
|
textContent: "No preview images found",
|
|
});
|
|
|
|
const imageCounterElement = utilitiesInstance.createDarkContainer();
|
|
|
|
imageCounterElement.style.top = "2%";
|
|
imageCounterElement.style.right = "2%";
|
|
imageCounterElement.appendChild(imageCounterLabel);
|
|
imageCounterElement.title = familiars.familiar_images.length > 0 ? "Click to set default image" : "No preview images found";
|
|
imageCounterElement.classList.add("JNodes-interactive-container");
|
|
|
|
// Click to set default image
|
|
imageCounterElement.addEventListener("click", setPreferredDefaultPreviewIndexToCurrent);
|
|
|
|
updateImageCounterText();
|
|
|
|
return imageCounterElement;
|
|
}
|
|
|
|
async function saveUserModelInfo(infoJson) {
|
|
|
|
const response = await api.fetchApi(
|
|
"/jnodes_save_model_user_info", {
|
|
method: "POST",
|
|
body: JSON.stringify(
|
|
{ "type": type, "item_name": familiars.full_name, "text": infoJson }
|
|
),
|
|
cache: "no-store"
|
|
}
|
|
);
|
|
|
|
try {
|
|
// Decode into a string
|
|
const decodedString = await utilitiesInstance.decodeReadableStream(response.body);
|
|
|
|
const asJson = JSON.parse(decodedString);
|
|
|
|
if (asJson?.success) {
|
|
parseUserCardInfo(infoJson);
|
|
modelElement.reinstantiate();
|
|
}
|
|
|
|
} catch (e) {
|
|
console.error(`Could not save user info for "${familiars.full_name}" because ${e}: ${infoJson}`)
|
|
}
|
|
}
|
|
|
|
function createButtonToolbar() {
|
|
|
|
function createButtons() {
|
|
if (!buttonsRow) { return; }
|
|
|
|
function createButton(foregroundElement, tooltipText, onClickFunction, dragText) {
|
|
const buttonElement = $el("button.JNodes-extra-networks-toolbar-button", {
|
|
title: tooltipText,
|
|
style: {
|
|
background: "none",
|
|
border: "none",
|
|
padding: 0,
|
|
}
|
|
}, [
|
|
foregroundElement
|
|
]);
|
|
|
|
if (onClickFunction) {
|
|
buttonElement.addEventListener("click", onClickFunction);
|
|
}
|
|
|
|
if (dragText) {
|
|
buttonElement.draggable = true;
|
|
buttonElement.addEventListener("dragstart", function (event) {
|
|
// Set data to be transferred during drag
|
|
event.dataTransfer.setData("text/plain", dragText);
|
|
});
|
|
}
|
|
|
|
buttonElement.classList.add("JNodes-interactive-container"); // Creates highlighting and mouse down color changes for feedback
|
|
|
|
return buttonElement;
|
|
}
|
|
|
|
const getModelInfo = (nameOverride = undefined) => {
|
|
|
|
return {
|
|
modelName: nameOverride ? nameOverride : cardInfo.nameText, // Dynamic model name
|
|
strengthModel: userCardInfoOverrides.defaultWeightModel, // Default model weight
|
|
strengthClip: userCardInfoOverrides.defaultWeightClip // Default clip weight
|
|
};
|
|
};
|
|
|
|
const getModelText = () => {
|
|
// Check if Lora is enabled
|
|
if (!bIsLora) {
|
|
return `(embedding:${cardInfo.nameText}:1)`; // Non-Lora text pattern
|
|
}
|
|
|
|
// Define replacement values
|
|
const replacements = getModelInfo();
|
|
|
|
// Access the template pattern
|
|
// local model override is the first priority, then model rules, then settings default, then fallback pattern
|
|
|
|
const settingPattern =
|
|
userCardInfoOverrides.loraTextPatternOverride ||
|
|
getCopyLoraTextPattern() ||
|
|
"<lora:{{modelName}}:{{strengthModel}}:{{strengthClip}}>";
|
|
|
|
// Replace placeholders with values
|
|
const formatted = settingPattern.replace(/{{(.*?)}}/g, (match, key) => {
|
|
return replacements[key] !== undefined ? replacements[key] : match; // Fallback to original placeholder if no match
|
|
});
|
|
|
|
return formatted; // Return the formatted string
|
|
};
|
|
|
|
let trainedWords = getTrainedWords();
|
|
|
|
if (bIsLora && trainedWords && trainedWords.length > 0) {
|
|
const getCopyAllText = () => {
|
|
return getModelText() + " " + trainedWords;
|
|
};
|
|
|
|
// Copy Lora text + trained words button
|
|
buttonsRow.appendChild(
|
|
createButton(
|
|
$el("label", {
|
|
textContent: "📋",
|
|
style: {
|
|
cursor: "pointer",
|
|
}
|
|
}),
|
|
`Copy lora as a1111-style text + trained words (${getCopyAllText()})\nDrag into a textarea to insert at cursor`,
|
|
function (e) {
|
|
|
|
utilitiesInstance.copyToClipboard(getCopyAllText());
|
|
e.preventDefault();
|
|
},
|
|
JSON.stringify({modelInsertText: getCopyAllText()})
|
|
)
|
|
);
|
|
|
|
// Copy trained words button
|
|
buttonsRow.appendChild(
|
|
createButton(
|
|
$el("label", {
|
|
textContent: "📝",
|
|
style: {
|
|
cursor: "pointer",
|
|
}
|
|
}),
|
|
`Copy trained words (${trainedWords})\nDrag into a textarea to insert at cursor`,
|
|
function (e) {
|
|
utilitiesInstance.copyToClipboard(trainedWords)
|
|
e.preventDefault();
|
|
},
|
|
JSON.stringify({modelInsertText: trainedWords})
|
|
)
|
|
);
|
|
}
|
|
|
|
// Copy model text button
|
|
buttonsRow.appendChild(
|
|
createButton(
|
|
$el("label", {
|
|
textContent: "📜",
|
|
style: {
|
|
cursor: "pointer",
|
|
}
|
|
}),
|
|
bIsLora ? `Copy lora as a1111-style text (${getModelText()})\nDrag into a textarea to insert at cursor` : `Copy embedding as comfy-style text (${getModelText()})\nDrag into a textarea to insert at cursor`,
|
|
function (e) {
|
|
utilitiesInstance.copyToClipboard(getModelText())
|
|
e.preventDefault();
|
|
},
|
|
JSON.stringify({modelInsertText: getModelText()})
|
|
)
|
|
);
|
|
|
|
if (bIsLora) {
|
|
// Drag node button
|
|
buttonsRow.appendChild(createButton(
|
|
$el("label", {
|
|
textContent: "📓",
|
|
style: {
|
|
cursor: "grab",
|
|
}
|
|
}),
|
|
`Drag the current model into the graph to create a node with the name ${familiars.full_name}`,
|
|
undefined,
|
|
JSON.stringify({modelInfo: getModelInfo(familiars.full_name)})
|
|
));
|
|
}
|
|
|
|
// Go to model link
|
|
if (cardInfo.modelId || userCardInfoOverrides.linkOverride) {
|
|
const href = getModelLink();
|
|
buttonsRow.appendChild(
|
|
createButton(
|
|
$el("a", {
|
|
target: "_blank",
|
|
href,
|
|
textContent: "🔗",
|
|
style: {
|
|
cursor: "pointer",
|
|
}
|
|
}),
|
|
`View model in browser (${href})`
|
|
)
|
|
);
|
|
}
|
|
|
|
// Paste image/video URI
|
|
buttonsRow.appendChild(
|
|
createButton(
|
|
$el("label", {
|
|
textContent: "🖼️",
|
|
style: {
|
|
cursor: "pointer",
|
|
}
|
|
}),
|
|
`Paste image or video URI/URL from the clipboard to add a new preview image and set it as default. ` +
|
|
`Any unsecured image or video should work, just right-click->Copy image/video address`,
|
|
modelElement.pasteImageOrVideoLinkAsPreview
|
|
)
|
|
);
|
|
|
|
// Edit model user data button
|
|
buttonsRow.appendChild(
|
|
createButton(
|
|
$el("label", {
|
|
textContent: "✏️",
|
|
style: {
|
|
cursor: "pointer",
|
|
}
|
|
}),
|
|
`Edit user-set model information`,
|
|
function (e) {
|
|
const modelEditForm = new ModelEditForm(imageDrawerInstance);
|
|
const modalManager = new ModalManager(imageDrawerInstance);
|
|
const formInfo = new FormInfo();
|
|
formInfo.itemToEdit = cardInfo.nameText;
|
|
formInfo.buttonText = "Save";
|
|
formInfo.onButtonClickFunction = async (formResponse) => {
|
|
|
|
if (formResponse) {
|
|
await saveUserModelInfo(formResponse);
|
|
modalManager.closeModal();
|
|
}
|
|
};
|
|
|
|
modalManager.createModal(modelEditForm.createForm(getUserCardInfoFormObjects(), formInfo));
|
|
|
|
// Remove focus from the currently focused element
|
|
document.activeElement.blur();
|
|
|
|
modelEditForm.focusFirstTextElement();
|
|
}
|
|
)
|
|
);
|
|
|
|
// Open in file manager
|
|
{
|
|
const baseLabelText = "📂";
|
|
buttonsRow.appendChild(
|
|
createButton(
|
|
$el("label", {
|
|
textContent: baseLabelText,
|
|
style: {
|
|
cursor: "pointer",
|
|
}
|
|
}),
|
|
"Open this file's containing directory in your OS's default file manager.",
|
|
(e) => {
|
|
modelElement.showInFileManager();
|
|
}
|
|
)
|
|
);
|
|
}
|
|
|
|
// ✉️ Send current image to metadata viewer
|
|
{
|
|
const baseLabelText = "✉️";
|
|
buttonsRow.appendChild(
|
|
createButton(
|
|
$el("label", {
|
|
textContent: baseLabelText,
|
|
style: {
|
|
cursor: "pointer",
|
|
}
|
|
}),
|
|
"Send Current Image to Metadata Viewer: Send this to any drawer instance with the 'Metadata Viewer' context currently selected. If it's in a sidebar, you may have to switch manually. " +
|
|
"If no drawer instance is using the 'Metadata Viewer' context, the current instance will change to it. " +
|
|
"Note that not all items will have metadata to show, so when switching it may seem that nothing has happened.",
|
|
async function (e) {
|
|
|
|
const metaDataContextName = "Metadata Viewer";
|
|
let metadataViewerInstance = findFirstImageDrawerInstanceWithGivenContext(metaDataContextName);
|
|
|
|
if (!metadataViewerInstance) { // Need to switch to it
|
|
|
|
const imageDrawerContextSelectorInstance = imageDrawerInstance.getComponentByName("ImageDrawerContextSelector");
|
|
imageDrawerContextSelectorInstance.setOptionSelected(metaDataContextName);
|
|
metadataViewerInstance = imageDrawerInstance;
|
|
}
|
|
|
|
if (metadataViewerInstance) {
|
|
|
|
const imageDrawerContextSelectorInstance = metadataViewerInstance.getComponentByName("ImageDrawerContextSelector");
|
|
const metadataViewerContextObject = imageDrawerContextSelectorInstance.getCurrentContextObject();
|
|
|
|
const response = await fetch(modelElement.fileInfo.imageHref);
|
|
let blob = await response.blob();
|
|
|
|
if (blob.type === "application/octet-stream" && modelElement.fileInfo.file?.format) {
|
|
blob = new Blob([blob], { type: modelElement.fileInfo.file.format });
|
|
}
|
|
|
|
const fileInfo = {
|
|
filename: modelElement.fileInfo.filename,
|
|
type: modelElement.fileInfo.type,
|
|
subfolder: modelElement.fileInfo.subdirectory || modelElement.fileInfo.subfolder || "",
|
|
};
|
|
metadataViewerContextObject.setImageOrVideo(blob, true, fileInfo);
|
|
}
|
|
}
|
|
)
|
|
);
|
|
}
|
|
|
|
// ✉️ Send model info to metadata viewer
|
|
{
|
|
const baseLabelText = "📦";
|
|
buttonsRow.appendChild(
|
|
createButton(
|
|
$el("label", {
|
|
textContent: baseLabelText,
|
|
style: {
|
|
cursor: "pointer",
|
|
}
|
|
}),
|
|
"Send Model Info to Metadata Viewer: Send this to any drawer instance with the 'Metadata Viewer' context currently selected. If it's in a sidebar, you may have to switch manually. " +
|
|
"If no drawer instance is using the 'Metadata Viewer' context, the current instance will change to it. " +
|
|
"Note that not all items will have metadata to show, so when switching it may seem that nothing has happened.",
|
|
async function (e) {
|
|
|
|
const metaDataContextName = "Metadata Viewer";
|
|
let metadataViewerInstance = findFirstImageDrawerInstanceWithGivenContext(metaDataContextName);
|
|
|
|
if (!metadataViewerInstance) { // Need to switch to it
|
|
|
|
const imageDrawerContextSelectorInstance = imageDrawerInstance.getComponentByName("ImageDrawerContextSelector");
|
|
imageDrawerContextSelectorInstance.setOptionSelected(metaDataContextName);
|
|
metadataViewerInstance = imageDrawerInstance;
|
|
}
|
|
|
|
if (metadataViewerInstance) {
|
|
|
|
const imageDrawerContextSelectorInstance = metadataViewerInstance.getComponentByName("ImageDrawerContextSelector");
|
|
const metadataViewerContextObject = imageDrawerContextSelectorInstance.getCurrentContextObject();
|
|
|
|
const response = await fetch(modelElement.fileInfo.imageHref);
|
|
let blob = await response.blob();
|
|
|
|
if (blob.type === "application/octet-stream" && modelElement.fileInfo.file?.format) {
|
|
blob = new Blob([blob], { type: modelElement.fileInfo.file.format });
|
|
}
|
|
|
|
let modelInfo = modelElement.fileInfo;
|
|
modelInfo.familiars = familiars;
|
|
modelInfo.name = getNameToUse();
|
|
|
|
metadataViewerContextObject.setModel(modelInfo, blob, true);
|
|
}
|
|
}
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
const buttonsRow = $el("div", {
|
|
style: {
|
|
width: "100%",
|
|
display: "flex",
|
|
flexDirection: "row",
|
|
}
|
|
});
|
|
|
|
const buttonToolbarContainerElement = utilitiesInstance.createDarkContainer();
|
|
|
|
buttonToolbarContainerElement.style.top = "2%";
|
|
buttonToolbarContainerElement.style.left = "2%";
|
|
buttonToolbarContainerElement.appendChild(buttonsRow);
|
|
|
|
createButtons();
|
|
|
|
return buttonToolbarContainerElement;
|
|
}
|
|
|
|
function createNameAndTagContainer() {
|
|
|
|
function createTagButton(tagName) {
|
|
const buttonElement = $el("button", {
|
|
title: `Click to search "${tagName}"`,
|
|
style: {
|
|
background: "none",
|
|
border: "none",
|
|
padding: "0",
|
|
}
|
|
}, [
|
|
$el("label", {
|
|
textContent: tagName,
|
|
style: {
|
|
fontSize: getLoraTextFontSize(1),
|
|
color: "rgb(250,250,250)",
|
|
wordBreak: "keep-all",
|
|
}
|
|
})
|
|
]);
|
|
|
|
buttonElement.addEventListener("click", async () => {
|
|
const imageDrawerSearchInstance = imageDrawerInstance.getComponentByName("ImageDrawerSearch");
|
|
imageDrawerSearchInstance.setSearchTextAndExecute(tagName);
|
|
});
|
|
|
|
return buttonElement;
|
|
}
|
|
|
|
const tagButtonContainer = $el("div", {
|
|
id: "tag-container",
|
|
style: {
|
|
display: "flex",
|
|
flexWrap: "wrap",
|
|
justifyContent: "center",
|
|
gap: "3.5%",
|
|
width: "90%",
|
|
}
|
|
});
|
|
|
|
const mainContainer = $el("div", {
|
|
id: "darkened-text-bg",
|
|
style: {
|
|
position: "absolute",
|
|
bottom: 0,
|
|
left: 0,
|
|
width: "100%",
|
|
backgroundColor: utilitiesInstance.getDarkColor(),
|
|
minHeight: "15%",
|
|
maxHeight: "90%",
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
alignItems: "center",
|
|
}
|
|
}, [
|
|
$el("label", {
|
|
textContent: getNameToUse(),
|
|
style: {
|
|
fontSize: getLoraTextFontSize(1.3),
|
|
top: "5%",
|
|
wordBreak: "break-all",
|
|
}
|
|
}),
|
|
tagButtonContainer
|
|
]);
|
|
|
|
for (const tag of getTags()) {
|
|
tagButtonContainer.appendChild(createTagButton(tag));
|
|
}
|
|
|
|
return mainContainer;
|
|
}
|
|
|
|
backgroundImageContainer.backgroundImage.forceLoad = function () {
|
|
|
|
backgroundImageContainer.backgroundVideo.style.display = "none";
|
|
backgroundImageContainer.backgroundImage.src = backgroundImageContainer.dataSrc;
|
|
backgroundImageContainer.backgroundImage.style.display = "block";
|
|
}
|
|
|
|
backgroundImageContainer.backgroundVideo.forceLoad = function () {
|
|
|
|
backgroundImageContainer.backgroundImage.style.display = "none";
|
|
backgroundImageContainer.backgroundVideo.src = backgroundImageContainer.dataSrc;
|
|
backgroundImageContainer.backgroundVideo.style.display = "block";
|
|
}
|
|
|
|
backgroundImageContainer.initVideo = function () {
|
|
|
|
backgroundImageContainer.backgroundVideo.autoplay = true;
|
|
backgroundImageContainer.backgroundVideo.loop = true;
|
|
backgroundImageContainer.backgroundVideo.controls = false;
|
|
backgroundImageContainer.backgroundVideo.muted = true;
|
|
}
|
|
backgroundImageContainer.initVideo();
|
|
|
|
modelElement.forceLoad = async function () {
|
|
|
|
if (await utilitiesInstance.isHrefVideo(backgroundImageContainer.dataSrc)) {
|
|
backgroundImageContainer.backgroundVideo.forceLoad();
|
|
} else {
|
|
backgroundImageContainer.backgroundImage.forceLoad();
|
|
}
|
|
}
|
|
|
|
modelElement.onObserverIntersect = function () {
|
|
modelElement.forceLoad();
|
|
}
|
|
|
|
modelElement.reinstantiate = async function() {
|
|
|
|
let bFoundUserInfo = false;
|
|
for (let familar_info of familiars.familiar_infos) {
|
|
|
|
if (familar_info.file_name.includes(".user.info")) {
|
|
|
|
familar_info.loaded_text = JSON.stringify(userCardInfoOverrides);
|
|
bFoundUserInfo = true;
|
|
}
|
|
}
|
|
|
|
if (!bFoundUserInfo) {
|
|
|
|
familiars.familiar_infos.push(
|
|
{ file_name: `${cardInfo.nameText}.user.info`, loaded_text: JSON.stringify(userCardInfoOverrides) } );
|
|
}
|
|
|
|
const newModelElement = await createExtraNetworkCard(cardInfo.nameText, familiars, type, imageDrawerInstance);
|
|
|
|
const imageDrawerListInstance = this.imageDrawerInstance.getComponentByName("ImageDrawerList");
|
|
imageDrawerListInstance.replaceImageListChild(this, newModelElement);
|
|
|
|
delete this;
|
|
}
|
|
|
|
modelElement.showInFileManager = async function () {
|
|
|
|
const call = `/jnodes_request_open_file_manager?filename=${encodeURIComponent(familiars.full_name)}&type=${type}`;
|
|
api.fetchApi(call, { method: "POST" });
|
|
}
|
|
|
|
modelElement.pasteImageOrVideoLinkAsPreview = async () => {
|
|
try {
|
|
const clipboardText = await navigator.clipboard.readText();
|
|
if (!clipboardText) {
|
|
console.error("Error reading clipboard");
|
|
return;
|
|
}
|
|
|
|
// Send the URL to the backend for processing
|
|
const response = await api.fetchApi(
|
|
`/jnodes_save_image_as_model_preview?filename=${encodeURIComponent(familiars.full_name)}&type=${type}`,
|
|
{
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ url: clipboardText }),
|
|
}
|
|
);
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
const newFilename = result.file_name;
|
|
|
|
familiars.familiar_images.push({file_name: `${familiars.containing_directory}/${newFilename}`});
|
|
|
|
backgroundImageContainer.lastViewedImageIndex = familiars.familiar_images.length - 1;
|
|
|
|
setPreferredDefaultPreviewIndexToCurrent();
|
|
|
|
modelElement.reinstantiate();
|
|
} else {
|
|
console.error(result.error);
|
|
}
|
|
} catch (error) {
|
|
console.error("Error receiving result from jnodes_save_image_as_model_preview:", error);
|
|
}
|
|
};
|
|
|
|
modelElement.appendChild(backgroundImageContainer);
|
|
const leftImageSwitchButton = createImageSwitchButton(true);
|
|
if (leftImageSwitchButton) { modelElement.appendChild(leftImageSwitchButton); }
|
|
const rightImageSwitchButton = createImageSwitchButton(false);
|
|
if (rightImageSwitchButton) { modelElement.appendChild(rightImageSwitchButton); }
|
|
modelElement.appendChild(createImageCounterElement());
|
|
modelElement.appendChild(createButtonToolbar());
|
|
modelElement.appendChild(createNameAndTagContainer());
|
|
|
|
modelElement.title = JSON.stringify(infoMap, null, "\t");
|
|
modelElement.imageDrawerInstance = imageDrawerInstance;
|
|
|
|
// Sorting meta information
|
|
modelElement.filename = cardInfo.nameText;
|
|
modelElement.friendlyName = getNameToUse();
|
|
modelElement.file_age = familiars.file_age;
|
|
modelElement.lora_meta = familiars.metadata;
|
|
modelElement.path = familiars.full_name;
|
|
|
|
modelElement.searchTerms =
|
|
`${familiars.full_name} ${modelElement.filename} ${modelElement.friendlyName} ${getTrainedWords()} ${getTags().join(" ")} ${modelElement.lora_meta}`;
|
|
|
|
modelElement.searchTerms = modelElement.searchTerms.toLowerCase().trim();
|
|
|
|
modelElement.getSearchTerms = function() { return modelElement.searchTerms; }
|
|
|
|
modelElement.draggable = true; // Made draggable to allow image drag and drop onto canvas / nodes / file explorer
|
|
|
|
modelElement.addEventListener('dragstart', function (event) {
|
|
event.dataTransfer.setData('text/jnodes_image_drawer_payload', `${JSON.stringify(modelElement.fileInfo)}`);
|
|
});
|
|
|
|
return modelElement;
|
|
} |