Files
Azornes-Comfyui-Model-Resolver/web/resolver/views/tabs_loaded_methods.js
T

501 lines
22 KiB
JavaScript

import { getSvgIcon } from "../../utils/icon_utils.js";
import {
getCustomNodeModelEntries,
getCustomNodeOriginalIdentity,
} from "../custom_nodes/registry.js";
import { normalizePathIdentity } from "../utils/html_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._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 = '<p>No workflow loaded. Please load a workflow first.</p>';
} else if (this.activeTab === 'loaded' && this.contentElement) {
this.contentElement.innerHTML = '<p>No workflow loaded. Please load a workflow first.</p>';
}
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.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 = `<p class="mr-error-text">Error: ${error.message}</p>`;
} else if (
!preserveContent &&
!loadToken &&
this.activeTab === 'loaded' &&
this.contentElement
) {
this.contentElement.innerHTML = `<p class="mr-error-text">Error: ${error.message}</p>`;
}
}
},
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 = {};
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: 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);
}
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 = '') => `
<button class="mr-btn-filter mr-btn-copy-compact ${extraClass}" onclick="window.MLCopy(${this.escapeJsString(text)}, this)">
${copyIcon}<span>Copy</span>
</button>`;
const countText = (count, label) => `${count} ${label}${count === 1 ? '' : 's'}`;
let html = `
<div class="mr-loaded-models-header">
<div class="mr-loaded-title-block">
<h3 class="mr-loaded-models-title">Loaded Models <span class="mr-loaded-total">${total}</span></h3>
<p class="mr-loaded-models-subtitle">${countText(activeCount, 'active')} / ${countText(inactiveCount, 'inactive')}</p>
</div>
<div class="mr-loaded-filter-row">
<button class="mr-btn-filter active" id="filter-all" onclick="window.MLFilterSwitch('all')">All (${activeCount + inactiveCount})</button>
<button class="mr-btn-filter" id="filter-active" onclick="window.MLFilterSwitch('active')">Active (${activeCount})</button>
<button class="mr-btn-filter" id="filter-inactive" onclick="window.MLFilterSwitch('inactive')">Inactive (${inactiveCount})</button>
</div>
</div>
<div class="mr-models-list mr-models-list-pad">
`;
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 += `<div class="mr-model-section" data-ml-filter="all" data-ml-category="${this.escapeHtml(category)}" data-ml-active="${hasActive}" data-ml-inactive="${hasInactive}">`;
html += `<div class="mr-model-section-header">
<div class="mr-model-section-heading">
<span class="mr-model-section-title">${this.escapeHtml(displayName.toUpperCase())}</span>
<span class="mr-model-section-total">${countText(sectionTotal, 'model')}</span>
</div>
<div class="mr-model-section-counts">
${hasActive ? `<span class="mr-model-count-pill is-active">${countText(modelsObj.active.length, 'active')}</span>` : ''}
${hasInactive ? `<span class="mr-model-count-pill is-inactive">${countText(modelsObj.inactive.length, 'inactive')}</span>` : ''}
</div>
</div>`;
if (hasActive) {
const activeStr = modelsObj.active.map(m => this.getModelToken(m, category)).join(' ');
html += `<div class="mr-model-group mr-model-group-active">
<div class="mr-model-group-head">
<span class="mr-model-group-label mr-model-group-label-active"><span class="mr-model-group-dot"></span>Active <span class="mr-model-group-count">${modelsObj.active.length}</span></span>
${copyButtonHtml(activeStr)}
</div>
<div class="mr-model-chip-list">`;
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 += `<span class="mr-model-chip" data-ml-loaded-model-key="${this.escapeHtml(modelKey)}"${this.getContextMenuAttrs(this.getLoadedModelContext(model))}${this.getModelPreviewTooltipAttrs(model, fullName)}>${this.escapeHtml(name)}${strength !== null ? `<span class="mr-model-chip-strength">${this.escapeHtml(strength)}</span>` : ''}</span>`;
}
html += `</div></div>`;
}
if (hasInactive) {
const inactiveStr = modelsObj.inactive.map(m => this.getModelToken(m, category)).join(' ');
html += `<div class="mr-model-group mr-model-group-inactive">
<div class="mr-model-group-head">
<span class="mr-model-group-label mr-model-group-label-inactive"><span class="mr-model-group-dot"></span>Inactive <span class="mr-model-group-count">${modelsObj.inactive.length}</span></span>
${copyButtonHtml(inactiveStr, 'is-muted')}
</div>
<div class="mr-model-chip-list mr-model-chip-list-inactive">`;
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 += `<span class="mr-model-chip" data-ml-loaded-model-key="${this.escapeHtml(modelKey)}"${this.getContextMenuAttrs(this.getLoadedModelContext(model))}${this.getModelPreviewTooltipAttrs(model, fullName)}>${this.escapeHtml(name)}${strength !== null ? `<span class="mr-model-chip-strength">${this.escapeHtml(strength)}</span>` : ''}</span>`;
}
html += `</div></div>`;
}
html += `</div>`;
}
const copySectionId = 'mr-copy-' + Date.now();
html += `
</div>
<div id="${copySectionId}" class="mr-copy-section" data-ml-active="${this.escapeHtml(activeString)}" data-ml-inactive="${this.escapeHtml(inactiveString)}" data-ml-all="${this.escapeHtml(allString)}" data-ml-active-count="${activeCount}" data-ml-inactive-count="${inactiveCount}" data-ml-all-count="${activeCount + inactiveCount}">
<div class="mr-copy-toolbar">
<div class="mr-copy-heading">
<div class="mr-copy-label" id="${copySectionId}-label">Copy all</div>
<div class="mr-copy-meta" id="${copySectionId}-meta">${countText(activeCount + inactiveCount, 'token')}</div>
</div>
<div class="mr-copy-actions">
<button type="button" class="mr-btn-filter mr-copy-mode active" data-ml-copy-mode="all" onclick="window.MLSetCopyMode('${copySectionId}', 'all', this)">All</button>
<button type="button" class="mr-btn-filter mr-copy-mode" data-ml-copy-mode="active" onclick="window.MLSetCopyMode('${copySectionId}', 'active', this)">Active</button>
<button type="button" class="mr-btn-filter mr-copy-mode" data-ml-copy-mode="inactive" onclick="window.MLSetCopyMode('${copySectionId}', 'inactive', this)">Inactive</button>
<button class="mr-btn-filter mr-copy-main-btn" onclick="window.MLCopyCode('${copySectionId}', this)">${copyIcon}<span>Copy</span></button>
</div>
</div>
<code class="mr-copy-code" id="${copySectionId}-code">${this.escapeHtml(allString)}</code>
</div>
`;
container.innerHTML = html;
// Store data on container for filter function
container.dataset.mlActiveString = activeString;
container.dataset.mlInactiveString = inactiveString;
container.dataset.mlAllString = allString;
}
};