import { getSvgIcon } from "../../utils/icon_utils.js"; import { getCustomNodeModelEntries, getCustomNodeOriginalIdentity, } from "../custom_nodes/registry.js"; import { normalizePathIdentity } from "../utils/html_utils.js"; import { buildLoadedModelTokenStrings, groupLoadedModelsByCategory, } from "../utils/loaded_model_utils.js"; import { initLoadedModelSelection, updateLoadedModelSelectionState, } from "../utils/loaded_model_selection.js"; export const tabsLoadedMethods = { getTabButton(tab) { return { missing: this.missingTab, loaded: this.loadedTab, metadata: this.metadataTab, options: this.optionsTab }[tab] || null; }, updateTabButtonStates() { ['missing', 'loaded', 'metadata', 'options'].forEach((tab) => { const button = this.getTabButton(tab); if (!button) return; const isActive = tab === this.activeTab; button.classList.toggle('mr-tab-active', isActive); button.setAttribute('aria-selected', isActive ? 'true' : 'false'); button.setAttribute('aria-disabled', isActive ? 'true' : 'false'); if (isActive) { button.setAttribute('aria-current', 'page'); } else { button.removeAttribute('aria-current'); } }); }, switchTab(tab, { force = false } = {}) { const nextTab = this.getValidTab(tab); const nextTabButton = this.getTabButton(nextTab); if ( this.activeTab === 'options' && nextTab !== 'options' && !this.confirmDiscardOptionsChanges() ) { return null; } if (!force && nextTab === this.activeTab && nextTabButton?.classList.contains('mr-tab-active')) { this.hideTooltip(); this.updateQueueVisibility(); return; } this.activeTab = nextTab; this.persistActiveTab(this.activeTab); if (this.activeTab !== 'missing') { this._analysisProgressToken = null; } if (this.activeTab !== 'loaded') { this._loadedModelsLoadToken = null; this._loadedModelsProgressToken = null; } if (this.activeTab !== 'loaded' && this.activeTab !== 'metadata') { if (typeof this._mlSelectionCleanup === 'function') { this._mlSelectionCleanup(); this._mlSelectionCleanup = null; } } if (this.activeTab !== 'metadata') { if (typeof this._metadataPasteCleanup === 'function') { this._metadataPasteCleanup(); this._metadataPasteCleanup = null; } } this.hideTooltip(); this.animateTabContentTransition(); this.updateTabButtonStates(); this.updateQueueVisibility(); if (this.activeTab === 'missing') { if (this.contentElement) { this.contentElement.style.overflowY = 'auto'; } this.setMissingFooterControlsVisible(true); return this.loadWorkflowData(); } else if (this.activeTab === 'loaded') { if (this.contentElement) { this.contentElement.style.overflowY = 'auto'; } this.setMissingFooterControlsVisible(false); return this.loadLoadedModels(); } else if (this.activeTab === 'metadata') { if (this.contentElement) { this.contentElement.style.overflowY = 'auto'; } this.setMissingFooterControlsVisible(false); return this.loadImageMetadata(); } else { if (this.contentElement) { this.contentElement.style.overflowY = 'hidden'; } this.setMissingFooterControlsVisible(false); this.displayOptions(); } return null; }, async loadLoadedModels( workflow = null, { force = false, preserveContent = false } = {} ) { if (!this.contentElement) return; this._loadedModelsLoadToken = null; this._loadedModelsProgressToken = null; let loadToken = null; const preservedScrollTop = preserveContent ? this.contentElement.scrollTop : null; const shouldRenderLoadedModels = () => ( this.activeTab === 'loaded' && loadToken && this._loadedModelsLoadToken === loadToken && this.contentElement ); try { workflow = workflow || this.getCurrentWorkflow(); if (!workflow) { if (shouldRenderLoadedModels()) { this.contentElement.innerHTML = '

No workflow loaded. Please load a workflow first.

'; } else if (this.activeTab === 'loaded' && this.contentElement) { this.contentElement.innerHTML = '

No workflow loaded. Please load a workflow first.

'; } return; } this.syncWorkflowScopedQueue(workflow); loadToken = `loaded-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; this._loadedModelsLoadToken = loadToken; const workflowSignature = this.getWorkflowSignature(workflow); const cachedLoadedModels = this.getCachedLoadedModelsForSignature?.( workflowSignature, { force }, ); if (cachedLoadedModels) { if (shouldRenderLoadedModels()) { this.displayLoadedModels(this.contentElement, cachedLoadedModels); } return; } const loadedProgressId = `loaded-progress-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; this._loadedModelsProgressToken = loadedProgressId; if (!preserveContent && shouldRenderLoadedModels()) { this.patchLoadedModelsProgress( this.contentElement, this.renderLoadedModelsProgress({ status: 'starting', stage: 'starting', message: 'Preparing loaded model scan...', percent: 0, current: 0, total: 0 }) ); } const progressPromise = preserveContent ? Promise.resolve() : this.pollLoadedModelsProgress(loadedProgressId, loadedProgressId); let data; try { data = await this.fetchJson('/model_resolver/loaded', { method: 'POST', body: JSON.stringify({ workflow, loaded_id: loadedProgressId }) }, 'Fetch loaded models'); } finally { if (this._loadedModelsProgressToken === loadedProgressId) { this._loadedModelsProgressToken = null; } await progressPromise; } if (this._loadedModelsLoadToken === loadToken) { this.cachedLoadedModelsSignature = workflowSignature; this.cachedLoadedModelsData = data; this.saveLoadedModelsCacheForActiveWorkflow(); } if (shouldRenderLoadedModels()) { this.displayLoadedModels(this.contentElement, data); if (preservedScrollTop !== null) { this.contentElement.scrollTop = preservedScrollTop; } } } catch (error) { console.error('Model Resolver: Error loading loaded models:', error); if (!preserveContent && shouldRenderLoadedModels()) { this.contentElement.innerHTML = `

Error: ${error.message}

`; } else if ( !preserveContent && !loadToken && this.activeTab === 'loaded' && this.contentElement ) { this.contentElement.innerHTML = `

Error: ${error.message}

`; } } }, getModelNameAndStrength(model) { const fullName = model.name || this.getFilenameFromPath(model.original_path) || 'Unknown'; const name = this.stripModelExtension(fullName); const strength = model.strength !== null && model.strength !== undefined ? model.strength.toFixed(2) : null; return { name, strength }; }, getModelToken(model, category) { const { name, strength } = this.getModelNameAndStrength(model); return `<${this.getCategoryTokenName(category)}:${name}:${strength || '1.00'}>`; }, getLoadedModelContext(model = {}) { return { ...model, context_scope: 'loaded_model' }; }, getLoadedModelDomKey(model = {}) { return this.getMissingModelKey(model); }, updateLoadedModelCopyValues(container, loadedModels = []) { if (!container) return; const byCategory = groupLoadedModelsByCategory(loadedModels); const strings = buildLoadedModelTokenStrings( byCategory, (model, category) => this.getModelToken(model, category), ); container.dataset.mlActiveString = strings.active; container.dataset.mlInactiveString = strings.inactive; container.dataset.mlAllString = strings.all; const copySection = container.querySelector?.('.mr-copy-section'); if (copySection) { copySection.dataset.mlActive = strings.active; copySection.dataset.mlInactive = strings.inactive; copySection.dataset.mlAll = strings.all; const activeMode = copySection.querySelector?.('.mr-copy-mode.active') ?.dataset?.mlCopyMode || 'all'; const copyCode = copySection.querySelector?.('.mr-copy-code'); if (copyCode) { if (activeMode === 'selected') { copyCode.textContent = copySection.dataset.mlSelected || ''; } else { copyCode.textContent = strings[activeMode] || strings.all; } } } const sections = container.querySelectorAll?.( '.mr-model-section[data-ml-category]' ) || []; for (const section of sections) { const category = section.dataset?.mlCategory || 'unknown'; const categoryModels = byCategory[category]; if (!categoryModels) continue; for (const filter of ['active', 'inactive']) { const button = section.querySelector?.( `.mr-model-group-${filter} .mr-btn-copy-compact` ); if (!button) continue; const text = categoryModels[filter] .map(model => this.getModelToken(model, category)) .join(' '); button.setAttribute( 'onclick', `window.MLCopy(${this.escapeJsString(text)}, this)` ); } } }, updateLoadedModelStrengthsFromNode(node) { if ( this.activeTab !== 'loaded' || !this.contentElement || !Array.isArray(this.cachedLoadedModelsData?.loaded_models) ) { return false; } const entries = getCustomNodeModelEntries(node) .filter(entry => Number.isFinite(entry.strength)) .map(entry => ({ identity: normalizePathIdentity(entry.identity), strength: entry.strength, })); if (!entries.length) return false; const nodeId = String(node?.id ?? ''); const modelBuckets = new Map(); for (const model of this.cachedLoadedModelsData.loaded_models) { if (String(model.node_id ?? '') !== nodeId) continue; const identity = normalizePathIdentity( getCustomNodeOriginalIdentity(model) || model.original_path || model.name || model.filename ); if (!identity) continue; if (!modelBuckets.has(identity)) { modelBuckets.set(identity, []); } modelBuckets.get(identity).push(model); } const chips = Array.from( this.contentElement.querySelectorAll?.('[data-ml-loaded-model-key]') || [] ); let updated = false; for (const entry of entries) { const model = modelBuckets.get(entry.identity)?.shift(); if (!model || Number(model.strength) === entry.strength) continue; model.strength = entry.strength; const modelKey = this.getLoadedModelDomKey(model); const chip = chips.find( candidate => candidate.dataset?.mlLoadedModelKey === modelKey ); const strengthElement = chip?.querySelector?.('.mr-model-chip-strength'); if (strengthElement) { strengthElement.textContent = entry.strength.toFixed(2); } if (chip && typeof this.getModelToken === 'function') { chip.dataset.mlToken = this.getModelToken(model, model.category || 'unknown'); } updated = true; } if (!updated) return false; this.updateLoadedModelCopyValues( this.contentElement, this.cachedLoadedModelsData.loaded_models ); this.updateLoadedModelSelectionState?.(this.contentElement); const workflow = this.getCurrentWorkflow?.(); const workflowSignature = workflow ? this.getWorkflowSignature?.(workflow) : ''; if (workflowSignature) { this.cachedLoadedModelsSignature = workflowSignature; this.activeWorkflowSignature = workflowSignature; } this.saveLoadedModelsCacheForActiveWorkflow?.(); return true; }, displayLoadedModels(container, data, options = {}) { const loadedModels = data.loaded_models || []; const total = data.total || 0; const title = String(options.title || 'Loaded Models'); const subtitle = options.subtitle ? String(options.subtitle) : ''; const includeContextMenu = options.includeContextMenu !== false; if (total === 0) { container.innerHTML = this.renderStatusMessage('No models found in workflow.', 'info'); return; } const byCategory = groupLoadedModelsByCategory(loadedModels); const activeCount = Object.values(byCategory).reduce((sum, cat) => sum + cat.active.length, 0); const inactiveCount = Object.values(byCategory).reduce((sum, cat) => sum + cat.inactive.length, 0); const { active: activeString, inactive: inactiveString, all: allString, } = buildLoadedModelTokenStrings( byCategory, (model, category) => this.getModelToken(model, category), { preserveEmptyCategories: true }, ); const copyIcon = getSvgIcon('copy', 'currentColor', 'mr-copy-btn-icon'); const copyButtonHtml = (text, extraClass = '') => ` `; const countText = (count, label) => `${count} ${label}${count === 1 ? '' : 's'}`; const modelContextAttrs = model => includeContextMenu ? this.getContextMenuAttrs(this.getLoadedModelContext(model)) : ''; const modelSubtitle = subtitle || `${countText(activeCount, 'active')} / ${countText(inactiveCount, 'inactive')}`; const isCollapsed = Boolean(options.isCollapsed); const tooltipText = options.tooltip || (options.collapsible && subtitle ? subtitle : ''); const tooltipHtml = tooltipText ? (typeof this.getMetadataTooltipBadge === 'function' ? this.getMetadataTooltipBadge(tooltipText, `${title} help`) : `?`) : ''; const titleHtml = options.collapsible ? `
${tooltipHtml}
` : `

${this.escapeHtml(title)} ${total}${tooltipHtml}

`; const showSubtitle = (options.showSubtitle ?? !options.collapsible) && Boolean(modelSubtitle); const subtitleHtml = showSubtitle ? `

${this.escapeHtml(modelSubtitle)}

` : ''; let html = `
${titleHtml} ${subtitleHtml}
`; for (const [category, modelsObj] of Object.entries(byCategory)) { const displayName = this.getCategoryDisplayName(category); const hasActive = modelsObj.active.length > 0; const hasInactive = modelsObj.inactive.length > 0; const sectionTotal = modelsObj.active.length + modelsObj.inactive.length; html += `
`; html += `
${this.escapeHtml(displayName.toUpperCase())} ${countText(sectionTotal, 'model')}
${hasActive ? `${countText(modelsObj.active.length, 'active')}` : ''} ${hasInactive ? `${countText(modelsObj.inactive.length, 'inactive')}` : ''}
`; if (hasActive) { const activeStr = modelsObj.active.map(m => this.getModelToken(m, category)).join(' '); html += `
Active ${modelsObj.active.length} ${copyButtonHtml(activeStr)}
`; for (const model of modelsObj.active) { const { name, strength } = this.getModelNameAndStrength(model); const fullName = model.original_path || model.name || name; const modelKey = this.getLoadedModelDomKey(model); const token = this.getModelToken(model, category); const tooltipLabel = model.model_name && model.model_name.toLowerCase() !== name.toLowerCase() ? `${model.model_name}${model.version_name ? ` (${model.version_name})` : ''} — ${fullName}` : fullName; html += `${this.escapeHtml(name)}${strength !== null ? `${this.escapeHtml(strength)}` : ''}`; } html += `
`; } if (hasInactive) { const inactiveStr = modelsObj.inactive.map(m => this.getModelToken(m, category)).join(' '); html += `
Inactive ${modelsObj.inactive.length} ${copyButtonHtml(inactiveStr, 'is-muted')}
`; for (const model of modelsObj.inactive) { const { name, strength } = this.getModelNameAndStrength(model); const fullName = model.original_path || model.name || name; const modelKey = this.getLoadedModelDomKey(model); const token = this.getModelToken(model, category); const tooltipLabel = model.model_name && model.model_name.toLowerCase() !== name.toLowerCase() ? `${model.model_name}${model.version_name ? ` (${model.version_name})` : ''} — ${fullName}` : fullName; html += `${this.escapeHtml(name)}${strength !== null ? `${this.escapeHtml(strength)}` : ''}`; } html += `
`; } html += `
`; } const copySectionId = 'mr-copy-' + Date.now(); html += `
Copy all
${countText(activeCount + inactiveCount, 'token')}
${this.escapeHtml(allString)}
`; container.innerHTML = html; // Store data on container for filter function container.dataset.mlActiveString = activeString; container.dataset.mlInactiveString = inactiveString; container.dataset.mlAllString = allString; if (typeof container?.querySelector === 'function') { this.bindLoadedModelSelection?.(container); this.bindTooltips?.(container); } }, bindLoadedModelSelection(container) { if (typeof this._mlSelectionCleanup === 'function') { this._mlSelectionCleanup(); this._mlSelectionCleanup = null; } const cleanup = initLoadedModelSelection(container, this); this._mlSelectionCleanup = cleanup; return cleanup; }, updateLoadedModelSelectionState(container) { return updateLoadedModelSelectionState(container); } };