import { getSvgIcon } from "../../utils/icon_utils.js"; import { escapeHtml } from "../utils/html_utils.js"; import { getCustomNodeModelEntries, getCustomNodeOriginalIdentity, } from "../custom_nodes/registry.js"; export const tabsLoadedMethods = { escapeHtml(value) { return escapeHtml(value); }, getTabButton(tab) { return { missing: this.missingTab, loaded: this.loadedTab, options: this.optionsTab }[tab] || null; }, updateTabButtonStates() { ['missing', 'loaded', '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 (!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; } 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.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); if ( !force && workflowSignature && this.cachedLoadedModelsSignature === workflowSignature && this.cachedLoadedModelsData ) { if (shouldRenderLoadedModels()) { this.displayLoadedModels(this.contentElement, this.cachedLoadedModelsData); } return; } const loadedProgressId = `loaded-progress-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; this._loadedModelsProgressToken = loadedProgressId; if (!preserveContent && shouldRenderLoadedModels()) { this.contentElement.innerHTML = 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); }, normalizeLoadedModelIdentity(value) { return String(value || '') .trim() .replace(/\\/g, '/') .toLowerCase(); }, updateLoadedModelCopyValues(container, loadedModels = []) { if (!container) return; const byCategory = {}; for (const model of loadedModels) { const category = model.category || 'unknown'; if (!byCategory[category]) { byCategory[category] = { active: [], inactive: [] }; } const filter = model.active !== false && model.connected !== false ? 'active' : 'inactive'; byCategory[category][filter].push(model); } const buildString = (filter) => Object.entries(byCategory) .flatMap(([category, models]) => { const selected = filter === 'all' ? [...models.active, ...models.inactive] : models[filter]; return selected.map(model => this.getModelToken(model, category)); }) .join(' '); const strings = { active: buildString('active'), inactive: buildString('inactive'), all: buildString('all'), }; 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) { 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: this.normalizeLoadedModelIdentity(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 = this.normalizeLoadedModelIdentity( 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); } updated = true; } if (!updated) return false; this.updateLoadedModelCopyValues( this.contentElement, this.cachedLoadedModelsData.loaded_models ); 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) { const loadedModels = data.loaded_models || []; const total = data.total || 0; if (total === 0) { container.innerHTML = this.renderStatusMessage('No models found in workflow.', 'info'); return; } const byCategory = {}; for (const model of loadedModels) { const cat = model.category || 'unknown'; if (!byCategory[cat]) { byCategory[cat] = { active: [], inactive: [] }; } // Some loaders expose per-model enablement separately from links. const isActive = model.active !== false && model.connected !== false; if (isActive) { byCategory[cat].active.push(model); } else { byCategory[cat].inactive.push(model); } } 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 buildCategoryStrings = (filter) => { const result = {}; for (const [category, modelsObj] of Object.entries(byCategory)) { const models = filter === 'active' ? modelsObj.active : filter === 'inactive' ? modelsObj.inactive : [...modelsObj.active, ...modelsObj.inactive]; const parts = models.map(model => this.getModelToken(model, category)); result[category] = parts.join(' '); } return Object.values(result).join(' '); }; const activeString = buildCategoryStrings('active'); const inactiveString = buildCategoryStrings('inactive'); const allString = buildCategoryStrings('all'); const copyIcon = getSvgIcon('copy', 'currentColor', 'mr-copy-btn-icon'); const copyButtonHtml = (text, extraClass = '') => ` `; const countText = (count, label) => `${count} ${label}${count === 1 ? '' : 's'}`; let html = `

Loaded Models ${total}

${countText(activeCount, 'active')} / ${countText(inactiveCount, 'inactive')}

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