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 tabsLoadedMethods = { 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.hideTooltip(); this.animateTabContentTransition(); this.updateTabButtonStates(); this.updateQueueVisibility(); if (this.activeTab === 'missing') { if (this.contentElement) { this.contentElement.style.overflowY = 'auto'; } this.setMissingFooterControlsVisible(true); this.loadWorkflowData(); } else if (this.activeTab === 'loaded') { if (this.contentElement) { this.contentElement.style.overflowY = 'auto'; } this.setMissingFooterControlsVisible(false); this.loadLoadedModels(); } else { if (this.contentElement) { this.contentElement.style.overflowY = 'hidden'; } this.setMissingFooterControlsVisible(false); this.displayOptions(); } }, async loadLoadedModels(workflow = null, { force = false } = {}) { if (!this.contentElement) return; this._loadedModelsLoadToken = null; let loadToken = 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; } if (shouldRenderLoadedModels()) { this.contentElement.innerHTML = 'Loading loaded models...
'; } const response = await api.fetchApi('/model_resolver/loaded', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ workflow }) }); if (!response.ok) { throw new Error(`API error: ${response.status}`); } const data = await response.json(); if (this._loadedModelsLoadToken === loadToken) { this.cachedLoadedModelsSignature = workflowSignature; this.cachedLoadedModelsData = data; this.saveLoadedModelsCacheForActiveWorkflow(); } if (shouldRenderLoadedModels()) { this.displayLoadedModels(this.contentElement, data); } } catch (error) { console.error('Model Resolver: Error loading loaded models:', error); if (shouldRenderLoadedModels()) { this.contentElement.innerHTML = `Error: ${error.message}
`; } else if (!loadToken && this.activeTab === 'loaded' && this.contentElement) { this.contentElement.innerHTML = `Error: ${error.message}
`; } } }, 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, e.g. LoraManager's // active flag and rgthree Power Lora Loader's on flag. 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 displayCat = this.getCategoryTokenName(category); const models = filter === 'active' ? modelsObj.active : filter === 'inactive' ? modelsObj.inactive : [...modelsObj.active, ...modelsObj.inactive]; const parts = models.map(model => { const fullName = model.name || model.original_path?.split(/[\/\\]/).pop() || 'Unknown'; let name = fullName; if (fullName.match(/\.(safetensors|ckpt|pt|pth|bin|pkl|sft|onnx|gguf)$/i)) { name = fullName.replace(/\.(safetensors|ckpt|pt|pth|bin|pkl|sft|onnx|gguf)$/i, ''); } const strength = model.strength !== null && model.strength !== undefined ? model.strength.toFixed(2) : '1.00'; return `<${displayCat}:${name}:${strength}>`; }); 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 stripModelExtension = (value) => String(value || 'Unknown').replace(/\.(safetensors|ckpt|pt|pth|bin|pkl|sft|onnx|gguf)$/i, ''); const modelToken = (model, category) => { const fullName = model.name || model.original_path?.split(/[\/\\]/).pop() || 'Unknown'; const name = stripModelExtension(fullName); const strength = model.strength !== null && model.strength !== undefined ? model.strength.toFixed(2) : '1.00'; return `<${this.getCategoryTokenName(category)}:${name}:${strength}>`; }; const copyButtonHtml = (text, extraClass = '') => ` `; const countText = (count, label) => `${count} ${label}${count === 1 ? '' : 's'}`; let html = `${countText(activeCount, 'active')} / ${countText(inactiveCount, 'inactive')}
${this.escapeHtml(allString)}