import { app } from "../../../../../scripts/app.js";
import { api } from "../../../../../scripts/api.js";
import { $el } from "../../../../../scripts/ui.js";
import { getSvgIcon } from "../../utils/icon_utils.js";
export const modelInfoMethods = {
/**
* Handle click outside context menu to hide it
*/
handleContextMenuOutsideClick(e) {
if (!this.contextMenu) return;
if (this.contextMenu.style.display === 'none') return;
// Check if click is outside the context menu
if (!this.contextMenu.contains(e.target)) {
this.hideContextMenu();
}
},
/**
* Show context menu at the specified position
*/
showContextMenu(x, y, model) {
if (!this.contextMenu) return;
this._contextMenuModel = model;
const canShowMore = this.canShowSourceDetails(model);
const isDownloadTableContext = model?.context_scope === 'download_table';
const isDownloadFolderContext = model?.context_scope === 'download_folder';
const hasLocalPath = Boolean(model?.folder_path || model?.download_directory || model?.directory || model?.path || model?.resolved_path);
const showOpenFolder = !isDownloadTableContext && hasLocalPath;
this.setContextMenuItemVisible('showInfo', !isDownloadTableContext && !isDownloadFolderContext);
this.setContextMenuItemVisible('showMore', canShowMore);
this.setContextMenuItemVisible('civitai', !isDownloadTableContext && !isDownloadFolderContext);
this.setContextMenuItemVisible('openFolder', showOpenFolder);
this.setContextMenuDividerVisible('source', (!isDownloadTableContext && !isDownloadFolderContext) || canShowMore);
this.setContextMenuDividerVisible('folder', showOpenFolder && !isDownloadFolderContext);
const openFolderLabel = this.contextMenu.querySelector('.mr-context-menu-action-open-folder span:last-child');
if (openFolderLabel) {
openFolderLabel.textContent = isDownloadFolderContext ? 'Open Download Folder' : 'Open Containing Folder';
}
// Position the menu
this.contextMenu.style.left = `${x}px`;
this.contextMenu.style.top = `${y}px`;
this.contextMenu.style.display = 'block';
// Adjust position if menu would go off screen
const rect = this.contextMenu.getBoundingClientRect();
if (rect.right > window.innerWidth) {
this.contextMenu.style.left = `${window.innerWidth - rect.width - 10}px`;
}
if (rect.bottom > window.innerHeight) {
this.contextMenu.style.top = `${window.innerHeight - rect.height - 10}px`;
}
},
/**
* Hide context menu
*/
hideContextMenu() {
if (!this.contextMenu) return;
this.contextMenu.style.display = 'none';
this._contextMenuModel = null;
},
/**
* Handle context menu item click
*/
handleContextMenuAction(action) {
const model = this._contextMenuModel;
this.hideContextMenu();
if (!model) return;
if (action === 'civitai') {
this.openInCivitAI(model);
} else if (action === 'openFolder') {
this.openContainingFolder(model);
} else if (action === 'showInfo') {
this.showModelInfo(model);
} else if (action === 'showMore') {
this.showSourceModelDetails(model);
}
},
setContextMenuItemVisible(action, visible) {
if (!this.contextMenu) return;
const className = `mr-context-menu-action-${String(action).replace(/[A-Z]/g, match => `-${match.toLowerCase()}`)}`;
this.contextMenu.querySelectorAll(`[data-menu-action="${action}"], .${className}`).forEach(item => {
item.hidden = !visible;
item.classList.toggle('mr-context-menu-hidden', !visible);
if (visible) {
item.style.removeProperty('display');
} else {
item.style.setProperty('display', 'none', 'important');
}
});
},
setContextMenuDividerVisible(name, visible) {
if (!this.contextMenu) return;
this.contextMenu.querySelectorAll(`[data-menu-divider="${name}"], .mr-context-menu-divider-${name}`).forEach(item => {
item.hidden = !visible;
item.classList.toggle('mr-context-menu-hidden', !visible);
if (visible) {
item.style.removeProperty('display');
} else {
item.style.setProperty('display', 'none', 'important');
}
});
},
canShowSourceDetails(model = {}) {
const source = String(model.details_source || model.source || '').toLowerCase();
return model?.context_scope === 'download_table'
&& ['civitai', 'civarchive', 'lora_manager_archive'].includes(source)
&& Boolean(model.model_id || model.modelId);
},
getMissingByKey(key = '') {
if (!key) return null;
return (this.missingModels || []).find(missing => this.getMissingModelKey?.(missing) === key) || null;
},
async openContainingFolder(model) {
const path = model?.folder_path || model?.download_directory || model?.directory || model?.path || model?.resolved_path || '';
if (!path) {
this.showNotification('No local file path available', 'error');
return;
}
try {
const response = await api.fetchApi('/model_resolver/open-containing-folder', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path })
});
if (!response.ok) {
throw new Error(`Open folder failed: ${response.status}`);
}
} catch (error) {
console.error('Model Resolver: Open folder error:', error);
this.showNotification('Failed to open containing folder', 'error');
}
},
/**
* Open model in CivitAI
*/
async openInCivitAI(model) {
if (!model) return;
const directUrl = this.getKnownCivitaiModelUrl(model);
if (directUrl) {
window.open(directUrl, '_blank');
return;
}
const name = model.name || model.original_path?.split(/[\/\\]/).pop() || '';
if (!name) return;
try {
// Search CivitAI for this model using hash (pass resolved_path for hash lookup)
const response = await api.fetchApi('/model_resolver/civitai-search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filename: name,
category: model.category,
resolved_path: model.resolved_path || ''
})
});
if (!response.ok) {
this.showNotification('Nie znaleziono modelu na CivitAI', 'error');
return;
}
const data = await response.json();
if (data.url) {
window.open(data.url, '_blank');
} else {
// Try direct search on CivitAI
const searchName = name.replace(/\.(safetensors|ckpt|pt|pth|bin|pkl|sft|onnx|gguf)$/i, '');
const searchUrl = `https://civitai.com/search?q=${encodeURIComponent(searchName)}`;
window.open(searchUrl, '_blank');
}
} catch (e) {
console.error('Model Resolver: Error searching CivitAI:', e);
// Fall back to direct search
const searchName = name.replace(/\.(safetensors|ckpt|pt|pth|bin|pkl|sft|onnx|gguf)$/i, '');
const searchUrl = `https://civitai.com/search?q=${encodeURIComponent(searchName)}`;
window.open(searchUrl, '_blank');
}
},
getKnownCivitaiModelUrl(model = {}) {
const candidates = [
model.model_url,
model.workflow_model_url,
model.openUrl,
model.url
];
for (const url of candidates) {
const value = String(url || '');
if (/^https:\/\/(?:www\.)?civitai\.com\/models\/\d+/i.test(value)) {
return value;
}
}
const modelId = model.model_id || model.modelId;
const versionId = model.version_id || model.versionId;
if (modelId) {
return `https://civitai.com/models/${modelId}${versionId ? `?modelVersionId=${versionId}` : ''}`;
}
return '';
},
/**
* Show model info dialog (similar to rgthree's RgthreeLoraInfoDialog)
*/
async showModelInfo(model) {
if (!model) return;
const name = model.name || model.original_path?.split(/[\/\\]/).pop() || '';
if (!name) return;
// Create and show the info dialog
this.showModelInfoDialog(name, model);
},
/**
* Show the model info dialog
*/
showModelInfoDialog(loraName, modelData) {
// Create info dialog element
const dialog = this.createInfoDialog(loraName, modelData);
this.restoreInfoDialogSize(dialog);
// Show the dialog
document.body.appendChild(dialog);
this.bindTooltips(dialog);
this.bindInfoDialogResizePersistence(dialog);
// Add close handlers
const closeBtn = dialog.querySelector('.mr-info-dialog-close');
const footerCloseBtn = dialog.querySelector('.mr-info-dialog-close-btn');
const backdrop = dialog.querySelector('.mr-info-dialog-backdrop');
if (closeBtn) {
closeBtn.addEventListener('click', () => this.closeInfoDialog(dialog));
}
if (footerCloseBtn) {
footerCloseBtn.addEventListener('click', () => this.closeInfoDialog(dialog));
}
if (backdrop) {
backdrop.addEventListener('click', (e) => {
// Only close if clicking backdrop itself, not its children
if (e.target === backdrop) {
this.closeInfoDialog(dialog);
}
});
}
// Fetch CivitAI info
this.fetchModelInfoForDialog(loraName, modelData, dialog);
},
/**
* Create the info dialog element
*/
createInfoDialog(loraName, modelData) {
const loraDisplayName = loraName.replace(/\.(safetensors|ckpt|pt|pth|bin|pkl|sft|onnx|gguf)$/i, '');
const dialog = document.createElement('div');
dialog.className = 'mr-info-dialog-backdrop';
dialog._selectedTrainedWords = new Set();
dialog.innerHTML = `
Loading...
| File ? |
|
| Hash (sha256) ? |
|
| CivitAI ? |
|
| Name ? |
|
| Base Model ? |
|
|
Trained Words ?
0 selected
|
Click words to select them.
|
| Clip Skip ? |
|
| Description ? |
|
`;
return dialog;
},
escapeHtml(value) {
return String(value ?? '')
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
},
escapeJsString(value) {
return this.escapeHtml(JSON.stringify(String(value ?? '')));
},
truncateText(value, maxLength = 160) {
const text = String(value ?? '').trim();
if (!text) return '';
return text.length > maxLength ? `${text.substring(0, maxLength)}...` : text;
},
sanitizeDescriptionHtml(html) {
const raw = String(html ?? '').trim();
if (!raw) return '';
const parser = new DOMParser();
const doc = parser.parseFromString(`${raw}
`, 'text/html');
const root = doc.body.firstElementChild;
if (!root) return this.escapeHtml(raw);
const allowedTags = new Set([
'p', 'br', 'strong', 'b', 'em', 'i', 'u', 's', 'code', 'pre',
'blockquote', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'a', 'span'
]);
const allowedStyles = new Set(['color']);
const sanitizeNode = (node) => {
if (node.nodeType === Node.TEXT_NODE) {
return;
}
if (node.nodeType !== Node.ELEMENT_NODE) {
node.remove();
return;
}
const tag = node.tagName.toLowerCase();
if (!allowedTags.has(tag)) {
const parent = node.parentNode;
if (!parent) {
node.remove();
return;
}
while (node.firstChild) {
parent.insertBefore(node.firstChild, node);
}
parent.removeChild(node);
return;
}
const attrs = Array.from(node.attributes);
for (const attr of attrs) {
const name = attr.name.toLowerCase();
if (name.startsWith('on') || name === 'id' || name === 'class' || name.startsWith('data-')) {
node.removeAttribute(attr.name);
continue;
}
if (name === 'href' && tag === 'a') {
const href = node.getAttribute('href') || '';
if (!/^https?:\/\//i.test(href)) {
node.removeAttribute('href');
} else {
node.setAttribute('target', '_blank');
node.setAttribute('rel', 'noopener noreferrer');
}
continue;
}
if (name === 'style') {
const safeStyles = [];
const styleValue = node.getAttribute('style') || '';
for (const part of styleValue.split(';')) {
const [prop, value] = part.split(':').map(v => v?.trim());
if (!prop || !value) continue;
if (allowedStyles.has(prop.toLowerCase())) {
safeStyles.push(`${prop}: ${value}`);
}
}
if (safeStyles.length) {
node.setAttribute('style', safeStyles.join('; '));
} else {
node.removeAttribute('style');
}
continue;
}
if (!(tag === 'a' && (name === 'target' || name === 'rel')) ) {
node.removeAttribute(attr.name);
}
}
Array.from(node.childNodes).forEach(child => sanitizeNode(child));
};
Array.from(root.childNodes).forEach(child => sanitizeNode(child));
return root.innerHTML;
},
normalizeTrainedWords(words) {
if (Array.isArray(words)) {
return [...new Set(words.map(word => String(word || '').trim()).filter(Boolean))];
}
if (typeof words === 'string') {
return [...new Set(
words
.split(/[\n,|;]/)
.map(word => word.trim())
.filter(Boolean)
)];
}
return [];
},
updateSelectedTrainedWordsSummary(dialog) {
if (!dialog) return;
const countEl = dialog.querySelector('.mr-info-trained-words-count');
const copyBtn = dialog.querySelector('.mr-info-copy-trained-words');
const selected = dialog._selectedTrainedWords instanceof Set
? Array.from(dialog._selectedTrainedWords)
: [];
if (countEl) {
countEl.textContent = `${selected.length} selected`;
}
if (copyBtn) {
copyBtn.disabled = selected.length === 0;
copyBtn.textContent = 'Copy';
}
},
bindInfoDialogInteractions(dialog) {
if (!dialog || dialog.dataset.mlInfoBound === 'true') return;
dialog.dataset.mlInfoBound = 'true';
dialog.addEventListener('click', async (event) => {
const wordBtn = event.target.closest('.mr-info-trained-word');
if (wordBtn && dialog.contains(wordBtn)) {
const word = wordBtn.dataset.word || '';
if (word) {
if (!(dialog._selectedTrainedWords instanceof Set)) {
dialog._selectedTrainedWords = new Set();
}
if (dialog._selectedTrainedWords.has(word)) {
dialog._selectedTrainedWords.delete(word);
wordBtn.classList.remove('is-selected');
wordBtn.setAttribute('aria-pressed', 'false');
} else {
dialog._selectedTrainedWords.add(word);
wordBtn.classList.add('is-selected');
wordBtn.setAttribute('aria-pressed', 'true');
}
this.updateSelectedTrainedWordsSummary(dialog);
}
return;
}
const copyBtn = event.target.closest('.mr-info-copy-trained-words');
if (copyBtn && dialog.contains(copyBtn)) {
const words = dialog._selectedTrainedWords instanceof Set
? Array.from(dialog._selectedTrainedWords)
: [];
if (!words.length) return;
try {
await navigator.clipboard.writeText(words.join(', '));
copyBtn.textContent = 'Copied';
} catch (error) {
console.error('Model Resolver: Failed to copy trained words:', error);
copyBtn.textContent = 'Failed';
}
setTimeout(() => {
if (copyBtn.isConnected) {
copyBtn.textContent = 'Copy';
}
}, 1200);
return;
}
const descToggleBtn = event.target.closest('.mr-info-description-toggle');
if (descToggleBtn && dialog.contains(descToggleBtn)) {
const descEl = dialog.querySelector('.mr-info-description');
if (!descEl) return;
const isExpanded = descEl.classList.toggle('is-expanded');
descToggleBtn.textContent = isExpanded ? 'Show less' : 'Show more';
}
});
},
getInfoDialogElement(dialog) {
return dialog?.querySelector?.('.mr-info-dialog') || null;
},
restoreInfoDialogSize(dialog) {
const panel = this.getInfoDialogElement(dialog);
if (!panel) return;
try {
const saved = JSON.parse(localStorage.getItem('model_resolver_info_dialog_size') || 'null');
if (!saved || typeof saved !== 'object') return;
const width = Number(saved.w);
const height = Number(saved.h);
if (!Number.isFinite(width) || !Number.isFinite(height)) return;
const maxWidth = Math.floor(window.innerWidth * 0.9);
const maxHeight = Math.floor(window.innerHeight * 0.8);
const clampedWidth = Math.max(420, Math.min(width, maxWidth));
const clampedHeight = Math.max(320, Math.min(height, maxHeight));
panel.style.width = `${clampedWidth}px`;
panel.style.height = `${clampedHeight}px`;
} catch (error) {
console.warn('Model Resolver: Failed to restore info dialog size:', error);
}
},
saveInfoDialogSize(dialog) {
const panel = this.getInfoDialogElement(dialog);
if (!panel) return;
const rect = panel.getBoundingClientRect();
const width = Math.round(rect.width);
const height = Math.round(rect.height);
if (!width || !height) return;
try {
localStorage.setItem('model_resolver_info_dialog_size', JSON.stringify({ w: width, h: height }));
} catch (error) {
console.warn('Model Resolver: Failed to save info dialog size:', error);
}
},
bindInfoDialogResizePersistence(dialog) {
const panel = this.getInfoDialogElement(dialog);
if (!panel || typeof ResizeObserver === 'undefined') return;
if (dialog._infoDialogResizeObserver) return;
let resizeSaveTimer = null;
const observer = new ResizeObserver(() => {
clearTimeout(resizeSaveTimer);
resizeSaveTimer = setTimeout(() => this.saveInfoDialogSize(dialog), 180);
dialog._infoDialogResizeSaveTimer = resizeSaveTimer;
});
observer.observe(panel);
dialog._infoDialogResizeObserver = observer;
dialog._infoDialogResizeSaveTimer = resizeSaveTimer;
},
/**
* Fetch model info and update the dialog
*/
async fetchModelInfoForDialog(loraName, modelData, dialog) {
try {
const response = await api.fetchApi('/model_resolver/civitai-search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filename: loraName,
category: modelData?.category || '',
resolved_path: modelData?.resolved_path || ''
})
});
if (response.ok) {
const data = await response.json();
this.updateInfoDialogWithData(dialog, data);
} else {
this.updateInfoDialogError(dialog, 'Model not found on CivitAI');
}
} catch (e) {
console.error('Model Resolver: Error fetching model info:', e);
this.updateInfoDialogError(dialog, 'Error fetching info');
}
},
/**
* Update the info dialog with data
*/
updateInfoDialogWithData(dialog, data) {
const loadingDiv = dialog.querySelector('.mr-info-dialog-loading');
const bodyDiv = dialog.querySelector('.mr-info-dialog-body');
this.bindInfoDialogInteractions(dialog);
if (loadingDiv) loadingDiv.style.display = 'none';
if (bodyDiv) bodyDiv.style.display = 'block';
if (!data) {
this.updateInfoDialogError(dialog, 'No data received');
return;
}
// Update title
const titleEl = dialog.querySelector('.mr-info-dialog-title');
if (titleEl) {
const modelName = data.model_name || data.modelName || 'Unknown Model';
const versionName = data.version_name || data.versionName || '';
titleEl.innerHTML = this.renderVersionedModelNameHtml(modelName, versionName) || this.escapeHtml(modelName);
}
// Update type tag
const typeTag = dialog.querySelector('.mr-info-type');
if (typeTag) {
const modelType = data.model_type || data.modelType || '';
typeTag.textContent = modelType.toUpperCase();
typeTag.className = `mr-info-tag mr-info-type ${this.getModelTypeColorClass(modelType)}`;
}
// Update base model tag
const baseModelTag = dialog.querySelector('.mr-info-basemodel');
if (baseModelTag) {
const baseModel = data.base_model || data.baseModel || '';
baseModelTag.textContent = baseModel || '';
if (baseModel) {
baseModelTag.style.display = '';
baseModelTag.className = `mr-info-tag mr-info-basemodel -basemodel-${baseModel.toLowerCase().replace(/\s+/g, '-')}`;
} else {
baseModelTag.style.display = 'none';
}
}
// Update file
const fileEl = dialog.querySelector('.mr-info-file');
if (fileEl && data.filename) {
fileEl.textContent = data.filename;
}
// Update hash
const hashEl = dialog.querySelector('.mr-info-hash');
if (hashEl) {
hashEl.textContent = data.sha256 || data.hash || '';
}
// Update CivitAI link
const civitaiLinkEl = dialog.querySelector('.mr-info-civitai-link');
if (civitaiLinkEl) {
if (data.url || data.version_url) {
const url = data.version_url || data.url;
civitaiLinkEl.innerHTML = `
${getSvgIcon('civitai', 'currentColor', 'mr-info-civitai-logo')}
View on Civitai
`;
} else {
const searchName = data.model_name || data.modelName || 'Unknown';
civitaiLinkEl.innerHTML = `
Model not found
${this.getSearchIconHtml()} Search on CivitAI
`;
}
}
// Update name
const nameEl = dialog.querySelector('.mr-info-name');
if (nameEl) {
nameEl.textContent = data.model_name || data.modelName || '';
}
// Update base model row
const baseModelRowEl = dialog.querySelector('.mr-info-base-model');
if (baseModelRowEl) {
const baseModel = data.base_model || data.baseModel || '';
baseModelRowEl.textContent = baseModel;
const row = baseModelRowEl.closest('tr');
if (row && baseModel) {
row.style.display = '';
} else if (row) {
row.style.display = 'none';
}
}
// Update trained words
const trainedWordsEl = dialog.querySelector('.mr-info-trained-words');
if (trainedWordsEl) {
const words = this.normalizeTrainedWords(data.trained_words || data.trainedWords || []);
if (words.length > 0) {
dialog._selectedTrainedWords = new Set();
trainedWordsEl.innerHTML = `${words.map(word => `
`).join('')}
`;
const row = trainedWordsEl.closest('tr');
if (row) row.style.display = '';
this.bindTooltips(trainedWordsEl);
this.updateSelectedTrainedWordsSummary(dialog);
} else {
const row = trainedWordsEl.closest('tr');
if (row) row.style.display = 'none';
}
}
// Update clip skip
const clipSkipEl = dialog.querySelector('.mr-info-clip-skip');
if (clipSkipEl) {
const clipSkip = data.clip_skip || data.clipSkip;
if (clipSkip && clipSkip !== 'None') {
clipSkipEl.textContent = clipSkip;
const row = clipSkipEl.closest('tr');
if (row) row.style.display = '';
} else {
const row = clipSkipEl.closest('tr');
if (row) row.style.display = 'none';
}
}
// Update description
const descEl = dialog.querySelector('.mr-info-description');
if (descEl) {
const desc = data.description || data.model_description || data.modelDescription || '';
if (desc) {
const actionsEl = dialog.querySelector('.mr-info-description-actions');
const toggleBtn = dialog.querySelector('.mr-info-description-toggle');
let sanitizedHtml = '';
try {
sanitizedHtml = this.sanitizeDescriptionHtml(desc);
} catch (error) {
console.error('Model Resolver: Failed to sanitize description HTML:', error);
}
const fallbackText = this.escapeHtml(String(desc).replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim());
const finalHtml = sanitizedHtml && sanitizedHtml.trim() ? sanitizedHtml : `${fallbackText}
`;
const textOnly = finalHtml.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
descEl.innerHTML = finalHtml;
descEl.classList.remove('is-expanded');
const shouldCollapse = textOnly.length > 520 || finalHtml.length > 900;
if (actionsEl) {
actionsEl.style.display = shouldCollapse ? '' : 'none';
}
if (toggleBtn) {
toggleBtn.textContent = 'Show more';
}
if (!shouldCollapse) {
descEl.classList.add('is-expanded');
}
const row = descEl.closest('tr');
if (row) row.style.display = '';
} else {
const row = descEl.closest('tr');
if (row) row.style.display = 'none';
}
}
// Update images
this.updateInfoDialogImages(dialog, data.images || data.modelImages || []);
},
/**
* Update images in the info dialog
*/
updateInfoDialogImages(dialog, images) {
const imagesContainer = dialog.querySelector('.mr-info-images');
if (!imagesContainer) return;
if (!Array.isArray(images) || !images.length) {
dialog._infoImages = [];
imagesContainer.innerHTML = '';
return;
}
const visibleImages = images.slice(0, 8).filter(img => img?.url);
dialog._infoImages = visibleImages;
const renderImageCard = (img, index) => {
const captionParts = [];
if (img.civitaiUrl) {
captionParts.push(`civitai`);
}
if (img.seed) captionParts.push(` ${this.escapeHtml(img.seed)}`);
if (img.steps) captionParts.push(` ${this.escapeHtml(img.steps)}`);
if (img.cfg) captionParts.push(` ${this.escapeHtml(img.cfg)}`);
if (img.sampler) captionParts.push(` ${this.escapeHtml(img.sampler)}`);
if (img.model) captionParts.push(` ${this.escapeHtml(this.truncateText(img.model, 72))}`);
if (img.positive) captionParts.push(` ${this.escapeHtml(this.truncateText(img.positive, 180))}`);
if (img.negative) captionParts.push(` ${this.escapeHtml(this.truncateText(img.negative, 180))}`);
return `
${captionParts.join('')}
`;
};
let imagesHtml = '';
imagesHtml += visibleImages.map(renderImageCard).join('');
imagesHtml += '
';
imagesContainer.innerHTML = imagesHtml;
imagesContainer.querySelectorAll('.mr-info-image-preview-btn').forEach(button => {
button.addEventListener('click', () => {
const index = parseInt(button.dataset.imageIndex || '0', 10);
this.openInfoImagePreview(visibleImages, Number.isNaN(index) ? 0 : index);
});
});
},
getInfoImagePromptTags(image = {}) {
const prompt = String(image.positive || '').trim();
const explicitTags = Array.isArray(image.tags)
? image.tags.map(tag => String(tag || '').trim()).filter(Boolean)
: [];
const promptTags = prompt
? prompt
.split(/[,;\n]/)
.map(tag => tag.trim())
.filter(tag => tag && tag.length <= 64)
: [];
return [...new Set([...explicitTags, ...promptTags])].slice(0, 24);
},
getInfoImageMetadataRows(image = {}) {
const size = image.width && image.height ? `${image.width} x ${image.height}` : '';
const rows = [
['Seed', image.seed],
['Steps', image.steps],
['CFG', image.cfg],
['Sampler', image.sampler],
['Clip skip', image.clip_skip],
['Size', size],
['Model', image.model]
];
return rows.filter(([, value]) => value !== null && value !== undefined && String(value).trim() !== '');
},
getInfoImageResources(image = {}) {
const resources = Array.isArray(image.resources) ? image.resources : [];
return resources
.filter(resource => resource && typeof resource === 'object')
.map(resource => ({
name: resource.name || resource.modelName || resource.model || resource.hash || 'Resource',
type: resource.type || resource.modelType || resource.resourceType || '',
weight: resource.weight || resource.strength || resource.value || ''
}))
.filter(resource => resource.name);
},
renderInfoImagePreviewPrompt(label, value, copyKey) {
const text = String(value || '').trim();
if (!text) return '';
return `
${this.escapeHtml(label)}
${this.escapeHtml(text)}
`;
},
renderInfoImagePreviewContent(preview) {
const images = Array.isArray(preview?._images) ? preview._images : [];
const index = Math.max(0, Math.min(images.length - 1, preview?._imageIndex || 0));
const image = images[index] || {};
const tags = this.getInfoImagePromptTags(image);
const metadataRows = this.getInfoImageMetadataRows(image);
const resources = this.getInfoImageResources(image);
const hasPrevious = images.length > 1;
const positionText = images.length > 1 ? `${index + 1} / ${images.length}` : '1 image';
const tagsHtml = tags.length
? `${tags.map(tag => `${this.escapeHtml(tag)}`).join('')}
`
: 'No prompt tags available.
';
const metadataHtml = metadataRows.length
? metadataRows.map(([label, value]) => `
${this.escapeHtml(label)}
${this.escapeHtml(value)}
`).join('')
: 'No generation metadata available.
';
const resourcesHtml = resources.length
? resources.map(resource => `
${this.escapeHtml(resource.name)}
${this.escapeHtml([resource.type, resource.weight].filter(Boolean).join(' / '))}
`).join('')
: '';
return `
${this.escapeHtml(positionText)}
${hasPrevious ? `` : ''}
${hasPrevious ? `` : ''}
`;
},
openInfoImagePreview(images, startIndex = 0) {
const imageList = (Array.isArray(images) ? images : []).filter(img => img?.url);
if (!imageList.length) return;
this.closeInfoImagePreview();
const preview = document.createElement('div');
preview.className = 'mr-image-preview-backdrop';
preview._images = imageList;
preview._imageIndex = Math.max(0, Math.min(imageList.length - 1, startIndex));
preview._onKeyDown = (event) => {
if (event.key === 'Escape') {
this.closeInfoImagePreview(preview);
} else if (event.key === 'ArrowLeft') {
this.showInfoImagePreviewAt(preview, preview._imageIndex - 1);
} else if (event.key === 'ArrowRight') {
this.showInfoImagePreviewAt(preview, preview._imageIndex + 1);
}
};
preview.addEventListener('click', async (event) => {
if (event.target === preview) {
this.closeInfoImagePreview(preview);
return;
}
const actionEl = event.target.closest('[data-action]');
if (actionEl && preview.contains(actionEl)) {
const action = actionEl.dataset.action;
if (action === 'close') {
this.closeInfoImagePreview(preview);
} else if (action === 'previous') {
this.showInfoImagePreviewAt(preview, preview._imageIndex - 1);
} else if (action === 'next') {
this.showInfoImagePreviewAt(preview, preview._imageIndex + 1);
}
return;
}
const copyBtn = event.target.closest('.mr-image-preview-copy');
if (copyBtn && preview.contains(copyBtn)) {
await this.copyInfoImagePreviewText(preview, copyBtn);
}
});
document.body.appendChild(preview);
document.addEventListener('keydown', preview._onKeyDown);
this.updateInfoImagePreview(preview);
},
showInfoImagePreviewAt(preview, index) {
const images = Array.isArray(preview?._images) ? preview._images : [];
if (!preview || !images.length) return;
preview._imageIndex = (index + images.length) % images.length;
this.updateInfoImagePreview(preview);
},
updateInfoImagePreview(preview) {
if (!preview) return;
preview.innerHTML = this.renderInfoImagePreviewContent(preview);
this.bindTooltips(preview);
},
async copyInfoImagePreviewText(preview, button) {
const images = Array.isArray(preview?._images) ? preview._images : [];
const image = images[preview?._imageIndex || 0] || {};
const key = button?.dataset?.copyKey || '';
const text = key === 'positive'
? image.positive
: key === 'negative'
? image.negative
: [
image.positive ? `Prompt: ${image.positive}` : '',
image.negative ? `Negative prompt: ${image.negative}` : '',
image.seed ? `Seed: ${image.seed}` : '',
image.steps ? `Steps: ${image.steps}` : '',
image.cfg ? `CFG: ${image.cfg}` : '',
image.sampler ? `Sampler: ${image.sampler}` : '',
image.model ? `Model: ${image.model}` : ''
].filter(Boolean).join('\n');
if (!text) return;
try {
await navigator.clipboard.writeText(String(text));
button.classList.add('is-copied');
button.innerHTML = `${getSvgIcon('copy')} Copied`;
} catch (error) {
console.error('Model Resolver: Failed to copy image metadata:', error);
button.innerHTML = `${getSvgIcon('copy')} Failed`;
}
setTimeout(() => {
if (button.isConnected) {
button.classList.remove('is-copied');
button.innerHTML = `${getSvgIcon('copy')} Copy${key === 'all' ? ' all' : ''}`;
}
}, 1200);
},
closeInfoImagePreview(preview = null) {
const target = preview || document.querySelector('.mr-image-preview-backdrop');
if (!target) return;
if (target._onKeyDown) {
document.removeEventListener('keydown', target._onKeyDown);
target._onKeyDown = null;
}
target.remove();
},
async showSourceModelDetails(model = {}) {
if (!this.canShowSourceDetails(model)) {
this.showNotification?.('No detailed source page is available for this model.', 'info');
return;
}
this.closeSourceModelDetails();
const details = document.createElement('div');
details.className = 'mr-model-details-backdrop';
details._sourceModel = model;
details.innerHTML = this.renderSourceModelDetailsLoading(model);
document.body.appendChild(details);
this.bindSourceModelDetailsEvents(details);
try {
const tokens = this.getStoredTokens?.() || {};
const response = await api.fetchApi('/model_resolver/model-details', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
source: model.details_source || model.source,
model_id: model.model_id || model.modelId,
version_id: model.version_id || model.versionId,
civitai_key: tokens.civitai_key || ''
})
});
if (!response.ok) {
throw new Error(`Details request failed: ${response.status}`);
}
const data = await response.json();
details._detailsData = data;
details._selectedVersionId = String(data.selected_version?.id || data.version_id || '');
details.innerHTML = this.renderSourceModelDetails(data, model);
this.bindSourceModelDetailsEvents(details);
this.bindTooltips(details);
} catch (error) {
console.error('Model Resolver: model details error:', error);
details.innerHTML = this.renderSourceModelDetailsError(error);
this.bindSourceModelDetailsEvents(details);
}
},
closeSourceModelDetails() {
document.querySelectorAll('.mr-model-details-backdrop').forEach(element => element.remove());
},
renderSourceModelDetailsLoading(model = {}) {
const title = model.name || model.model_name || model.filename || 'Model details';
return `
${this.escapeHtml(title)}
${this.escapeHtml(model.details_source || model.source || '')}
Loading model details...
`;
},
renderSourceModelDetailsError(error) {
return `
Model details
Error
${this.escapeHtml(error?.message || 'Failed to load model details.')}
`;
},
async fetchSourceModelDetailsVersion(details, versionId) {
if (!details?._detailsData || !versionId) return;
try {
const data = details._detailsData;
const tokens = this.getStoredTokens?.() || {};
const response = await api.fetchApi('/model_resolver/model-details', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
source: data.source || details._sourceModel?.details_source || details._sourceModel?.source,
model_id: data.model_id || details._sourceModel?.model_id || details._sourceModel?.modelId,
version_id: versionId,
civitai_key: tokens.civitai_key || ''
})
});
if (!response.ok) {
throw new Error(`Version details request failed: ${response.status}`);
}
const fresh = await response.json();
const freshVersion = fresh.selected_version;
if (!freshVersion?.id) return;
data.versions = (data.versions || []).map(version =>
String(version.id) === String(freshVersion.id)
? { ...version, ...freshVersion }
: version
);
data.selected_version = data.versions.find(version => String(version.id) === String(freshVersion.id)) || freshVersion;
if (String(details._selectedVersionId || '') !== String(freshVersion.id)) return;
details.innerHTML = this.renderSourceModelDetails(data, details._sourceModel || {});
this.bindTooltips(details);
} catch (error) {
console.error('Model Resolver: failed to fetch source model version details:', error);
this.showNotification?.('Failed to load this model version.', 'error');
}
},
renderSourceModelDetails(data = {}, contextModel = {}) {
const versions = Array.isArray(data.versions) ? data.versions : [];
const selectedVersionId = String(data.selected_version?.id || data.version_id || versions[0]?.id || '');
const selectedVersion = versions.find(version => String(version.id) === selectedVersionId) || data.selected_version || versions[0] || {};
const images = (Array.isArray(selectedVersion.images) && selectedVersion.images.length ? selectedVersion.images : data.images || []).filter(img => img?.url).slice(0, 12);
const stats = data.stats || {};
const versionStats = selectedVersion.stats || {};
const tags = Array.isArray(data.tags) ? data.tags.slice(0, 12) : [];
const trainedWords = this.normalizeTrainedWords(selectedVersion.trained_words || []);
const description = data.description || selectedVersion.description || '';
const sanitizedDescription = description ? this.sanitizeDescriptionHtml(description) : '';
const source = data.source || contextModel.details_source || contextModel.source || '';
const creator = data.creator || {};
const creatorName = creator.username || creator.name || '';
const statItems = [
['Downloads', stats.downloadCount || stats.downloads || versionStats.downloadCount || versionStats.downloads],
['Thumbs Up', stats.thumbsUpCount || versionStats.thumbsUpCount],
['Rating', stats.rating || versionStats.rating],
['Published', selectedVersion.published_at ? this.formatDetailsDate(selectedVersion.published_at) : '']
].filter(([, value]) => value !== undefined && value !== null && String(value).trim() !== '');
return `
${this.escapeHtml(data.name || contextModel.name || 'Model')}
${this.escapeHtml([source, data.type, creatorName].filter(Boolean).join(' / '))}
${versions.map(version => `
`).join('')}
${images.length > 1 ? `
` : ''}
${images.length ? images.map((image, index) => `
`).join('') : '
No example images available.
'}
${images.length > 1 ? `
` : ''}
About
${sanitizedDescription ? sanitizedDescription : '
No description available.
'}
`;
},
renderSourceModelDetailsFiles(version = {}, data = {}, contextModel = {}) {
const files = Array.isArray(version.files) ? version.files.filter(file => file?.download_url) : [];
if (!files.length) {
return 'No downloadable files available for this version.
';
}
return files.map((file, index) => {
const fileMeta = this.getSourceModelFileMeta(file);
const mirrors = this.getSourceModelMirrors(file);
const payload = {
source: data.source || contextModel.details_source || contextModel.source,
model_id: data.model_id || contextModel.model_id,
version_id: version.id || data.version_id || contextModel.version_id,
name: data.name || contextModel.name,
version_name: version.name || '',
type: data.type || contextModel.type,
filename: file.name || contextModel.filename || data.name || 'model',
download_url: file.download_url,
url: version.url || data.version_url || data.url,
size: file.size,
base_model: version.base_model || contextModel.base_model,
tags: data.tags || [],
match_type: 'selected',
confidence: 100
};
const mirrorList = mirrors.length > 1
? this.renderSourceModelMirrors(mirrors, payload)
: '';
return `
${this.escapeHtml(file.name || `File ${index + 1}`)}
${this.escapeHtml(fileMeta.summary)}
${fileMeta.badges.length ? `
${fileMeta.badges.map(badge => `${this.escapeHtml(badge)}`).join('')}
` : ''}
${mirrorList}
`;
}).join('');
},
getSourceModelMirrors(file = {}) {
const mirrors = Array.isArray(file.mirrors)
? file.mirrors.filter(mirror => mirror?.url)
: [];
const downloadUrls = Array.isArray(file.download_urls) ? file.download_urls.filter(Boolean) : [];
const seen = new Set();
const normalized = [];
for (const mirror of mirrors) {
const url = String(mirror.url || '');
if (!url || seen.has(url)) continue;
seen.add(url);
normalized.push({
...mirror,
url,
filename: mirror.filename || file.name,
is_dead: Boolean(mirror.is_dead || mirror.isDead || mirror.deleted_at || mirror.deletedAt)
});
}
for (const url of downloadUrls) {
const value = String(url || '');
if (!value || seen.has(value)) continue;
seen.add(value);
normalized.push({
url: value,
filename: file.name,
source: this.getSourceModelMirrorHost(value),
is_dead: false
});
}
return normalized;
},
renderSourceModelMirrors(mirrors = [], basePayload = {}) {
if (!mirrors.length) return '';
const groups = this.groupSourceModelMirrors(mirrors);
return `
Mirrors
${groups.map(group => `
${this.getSourceModelMirrorIcon(group.label)}
${this.escapeHtml(group.label)} (${group.items.length} ${group.items.length === 1 ? 'mirror' : 'mirrors'})
${group.items.map((mirror) => {
const payload = {
...basePayload,
download_url: mirror.url,
filename: mirror.filename || basePayload.filename,
mirror_source: mirror.source || this.getSourceModelMirrorHost(mirror.url),
mirror_index: mirror._index
};
const meta = this.getSourceModelMirrorMeta(mirror, mirror._index === 0);
const isDead = this.isSourceModelMirrorDead(mirror);
const isGated = this.isSourceModelMirrorGated(mirror);
const title = isDead ? 'Likely dead link' : (isGated ? 'Gated Download' : 'Download file');
const actionIcon = getSvgIcon(isDead ? 'skull' : (isGated ? 'shield' : 'download'));
const rowClass = [
isDead ? 'is-dead' : '',
isGated && !isDead ? 'is-gated' : ''
].filter(Boolean).map(value => ` ${value}`).join('');
const payloadData = this.escapeHtml(encodeURIComponent(JSON.stringify(payload)));
return `
`;
}).join('')}
`).join('')}
`;
},
groupSourceModelMirrors(mirrors = []) {
const groups = new Map();
mirrors.forEach((mirror, index) => {
const label = this.getSourceModelMirrorSourceLabel(mirror, index);
if (!groups.has(label)) {
groups.set(label, { label, items: [] });
}
groups.get(label).items.push({ ...mirror, _index: index });
});
return Array.from(groups.values());
},
getSourceModelMirrorIcon(label = '') {
const normalized = String(label || '').toLowerCase();
if (normalized.includes('huggingface')) return getSvgIcon('huggingface');
if (normalized.includes('civitai')) return getSvgIcon('civitai');
return getSvgIcon('globe');
},
isSourceModelMirrorDead(mirror = {}) {
return Boolean(mirror.is_dead || mirror.isDead || mirror.deleted_at || mirror.deletedAt);
},
isSourceModelMirrorGated(mirror = {}) {
return Boolean(mirror.is_gated || mirror.isGated || mirror.gated);
},
getSourceModelMirrorHost(url = '') {
try {
return new URL(url).hostname.replace(/^www\./, '');
} catch (error) {
return '';
}
},
getSourceModelMirrorSourceLabel(mirror = {}, index = 0) {
const source = String(mirror.source || '').trim();
if (source) return source;
const host = this.getSourceModelMirrorHost(mirror.url || '');
if (host.includes('huggingface.co')) return 'HuggingFace';
if (host.includes('civitai.com')) return 'CivitAI';
return host || `Mirror ${index + 1}`;
},
getSourceModelMirrorMeta(mirror = {}, isDefault = false) {
const host = this.getSourceModelMirrorHost(mirror.url || '');
const meta = [
isDefault ? 'Default' : '',
this.isSourceModelMirrorDead(mirror) ? 'Likely dead' : '',
host,
this.isSourceModelMirrorGated(mirror) ? 'Gated' : '',
mirror.is_paid ? 'Paid' : '',
mirror.sha256 ? `SHA256 ${String(mirror.sha256).slice(0, 10)}` : ''
].filter(Boolean);
return meta.join(' / ');
},
getSourceModelFileMeta(file = {}) {
const metadata = file.metadata && typeof file.metadata === 'object' ? file.metadata : {};
const mirrorCount = Array.isArray(file.mirrors) && file.mirrors.length
? file.mirrors.length
: file.mirror_count;
const badges = [
file.primary ? 'Primary' : '',
file.type || '',
metadata.format || '',
metadata.size || '',
metadata.fp || '',
mirrorCount ? `${mirrorCount} mirrors` : '',
file.id ? `ID ${file.id}` : ''
].filter(Boolean);
const summary = [
this.formatSearchResultSize(file),
file.sha256 ? `SHA256 ${String(file.sha256).slice(0, 10)}` : ''
].filter(Boolean).join(' / ') || 'Downloadable file';
return { badges, summary };
},
renderSourceModelImageMeta(image = {}) {
const stats = image.stats || {};
const metaItems = [
['Like', image.likeCount || image.likes || stats.likeCount || stats.likes],
['Love', image.heartCount || image.reactions || stats.heartCount],
['Comments', image.commentCount || stats.commentCount],
['Buzz', image.buzzCount || stats.buzzCount]
].filter(([, value]) => value !== undefined && value !== null && String(value).trim() !== '');
if (!metaItems.length) return '';
return `
${metaItems.map(([label, value]) => `${this.escapeHtml(label)} ${this.escapeHtml(value)}`).join('')}
`;
},
getSourceModelImageSizingStyle(image = {}) {
const width = Number(image.width || image.metadata?.width || 0);
const height = Number(image.height || image.metadata?.height || 0);
const ratio = width > 0 && height > 0 ? width / height : 0.72;
const clampedRatio = Math.max(0.48, Math.min(1.85, ratio));
const preferredHeight = Math.max(
320,
Math.min(
560,
Math.round(430 + Math.max(0, 1 - clampedRatio) * 150 - Math.max(0, clampedRatio - 1) * 70)
)
);
let preferredWidth = Math.round(preferredHeight * clampedRatio);
if (preferredWidth < 220) {
preferredWidth = 220;
} else if (preferredWidth > 640) {
preferredWidth = 640;
}
const adjustedHeight = Math.round(preferredWidth / clampedRatio);
return `--mr-image-ratio:${clampedRatio.toFixed(3)}; --mr-image-width:${preferredWidth}px; --mr-image-height:${adjustedHeight}px;`;
},
bindSourceModelDetailsEvents(details) {
if (!details) return;
if (details.dataset.modelDetailsBound === 'true') return;
details.dataset.modelDetailsBound = 'true';
details.addEventListener('click', (event) => {
if (event.target === details) {
this.closeSourceModelDetails();
return;
}
const actionEl = event.target.closest('[data-action]');
if (actionEl?.dataset.action === 'close') {
this.closeSourceModelDetails();
return;
}
const versionTab = event.target.closest('.mr-model-details-version-tab');
if (versionTab && details.contains(versionTab)) {
details._selectedVersionId = String(versionTab.dataset.versionId || '');
const data = details._detailsData;
if (data) {
const selected = (data.versions || []).find(version => String(version.id) === details._selectedVersionId) || data.selected_version;
data.selected_version = selected;
details.innerHTML = this.renderSourceModelDetails(data, details._sourceModel || {});
this.bindTooltips(details);
if (selected?.id && (!Array.isArray(selected.files) || !selected.files.length)) {
this.fetchSourceModelDetailsVersion(details, selected.id);
}
}
return;
}
const galleryNav = event.target.closest('.mr-model-details-gallery-nav');
if (galleryNav && details.contains(galleryNav)) {
const strip = galleryNav.closest('.mr-model-details-gallery')?.querySelector('.mr-model-details-gallery-strip');
if (!strip) return;
const direction = Number(galleryNav.dataset.galleryDirection || 1);
strip.scrollBy({
left: direction * Math.max(260, Math.round(strip.clientWidth * 0.82)),
behavior: 'smooth'
});
return;
}
const imageBtn = event.target.closest('.mr-model-details-image');
if (imageBtn && details.contains(imageBtn)) {
if (details._detailsGalleryDraggedUntil && Date.now() < details._detailsGalleryDraggedUntil) {
return;
}
const data = details._detailsData || {};
const version = data.selected_version || {};
const images = (Array.isArray(version.images) && version.images.length ? version.images : data.images || []).filter(img => img?.url);
const index = parseInt(imageBtn.dataset.imageIndex || '0', 10);
this.openInfoImagePreview(images, Number.isNaN(index) ? 0 : index);
return;
}
const useBtn = event.target.closest('.mr-model-details-use');
if (useBtn && details.contains(useBtn)) {
try {
const selection = JSON.parse(decodeURIComponent(useBtn.dataset.selection || ''));
this.applySourceModelDetailsSelection(selection, details._sourceModel || {});
this.closeSourceModelDetails();
} catch (error) {
console.error('Model Resolver: failed to apply model detail selection:', error);
this.showNotification?.('Failed to select this model version.', 'error');
}
}
});
details.addEventListener('pointerdown', (event) => {
const strip = event.target.closest?.('.mr-model-details-gallery-strip');
if (!strip || !details.contains(strip) || event.button > 0) return;
const startX = event.clientX;
const startScrollLeft = strip.scrollLeft;
let moved = false;
strip.classList.add('is-dragging');
strip.setPointerCapture?.(event.pointerId);
const onPointerMove = (moveEvent) => {
const delta = moveEvent.clientX - startX;
if (Math.abs(delta) > 4) {
moved = true;
details._detailsGalleryDraggedUntil = Date.now() + 250;
}
strip.scrollLeft = startScrollLeft - delta;
};
const finishDrag = () => {
strip.classList.remove('is-dragging');
strip.removeEventListener('pointermove', onPointerMove);
strip.removeEventListener('pointerup', finishDrag);
strip.removeEventListener('pointercancel', finishDrag);
if (moved) {
details._detailsGalleryDraggedUntil = Date.now() + 250;
}
};
strip.addEventListener('pointermove', onPointerMove);
strip.addEventListener('pointerup', finishDrag, { once: true });
strip.addEventListener('pointercancel', finishDrag, { once: true });
});
},
applySourceModelDetailsSelection(selection = {}, contextModel = {}) {
const missing = this.getMissingByKey(contextModel.missing_key || contextModel.missingKey || '');
if (!missing || !selection.download_url) {
this.showNotification?.('Cannot apply this version to the current missing model.', 'error');
return;
}
const sourceKey = String(selection.source || contextModel.details_source || contextModel.source || '').toLowerCase();
const selectedBaseModel = selection.base_model || contextModel.base_model || '';
const sourceResult = {
source: sourceKey,
model_id: selection.model_id,
version_id: selection.version_id,
name: selection.name,
version_name: selection.version_name,
type: selection.type,
filename: selection.filename,
url: selection.url,
download_url: selection.download_url,
size: selection.size,
base_model: selectedBaseModel,
tags: selection.tags || [],
match_type: selection.match_type || 'selected',
confidence: selection.confidence || 100,
searchedAt: new Date().toISOString()
};
missing.download_source = {
...sourceResult,
url: sourceResult.download_url,
model_url: sourceResult.url,
directory: missing.category || 'checkpoints'
};
const state = this.getSearchState?.(missing);
if (state?.results && sourceKey) {
state.results[sourceKey] = sourceResult;
}
missing.civitai_info = {
...(missing.civitai_info || {}),
model_name: selection.name || missing.civitai_info?.model_name,
version_name: selection.version_name || missing.civitai_info?.version_name,
expected_filename: selection.filename || missing.civitai_info?.expected_filename,
base_model: selectedBaseModel || missing.civitai_info?.base_model,
tags: selection.tags || missing.civitai_info?.tags || []
};
if (selectedBaseModel) {
const canonicalBaseModel = this.resolveBaseModelAlias?.(selectedBaseModel) || selectedBaseModel;
if (state) {
state.selectedBaseModel = canonicalBaseModel;
state.lastAttemptBaseModelContext = canonicalBaseModel;
}
}
this.refreshSearchUiForMissing?.(missing, state || null);
this.refreshSearchBaseModelLabels?.();
this.updateBatchFooterButtons?.();
this.persistSearchStateForActiveWorkflow?.();
this.showNotification?.(`Selected ${selection.filename || selection.name || 'model version'}.`, 'success');
},
formatDetailsDate(value) {
if (!value) return '';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return String(value);
return date.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
},
/**
* Update the info dialog with error
*/
updateInfoDialogError(dialog, message) {
const civitaiLink = dialog.querySelector('.mr-info-civitai-link');
if (civitaiLink) {
civitaiLink.innerHTML = `${message}`;
}
},
/**
* Close the info dialog
*/
closeInfoDialog(dialog) {
if (dialog?._infoDialogResizeObserver) {
dialog._infoDialogResizeObserver.disconnect();
dialog._infoDialogResizeObserver = null;
}
if (dialog?._infoDialogResizeSaveTimer) {
clearTimeout(dialog._infoDialogResizeSaveTimer);
dialog._infoDialogResizeSaveTimer = null;
}
this.saveInfoDialogSize(dialog);
this.closeInfoImagePreview();
if (dialog && dialog.parentNode) {
dialog.parentNode.removeChild(dialog);
}
}
};