diff --git a/web/resolver/missing_type_filter.css b/web/resolver/missing_type_filter.css new file mode 100644 index 0000000..8042051 --- /dev/null +++ b/web/resolver/missing_type_filter.css @@ -0,0 +1,143 @@ +.mr-missing-type-filter-cell { + overflow: visible; +} + +.mr-missing-type-filter-wrap { + position: relative; + display: inline-flex; + min-width: 0; +} + +#model-resolver-modal .mr-missing-type-filter-button { + position: relative; + display: inline-flex; + align-items: center; + gap: 5px; + max-width: 100%; + min-height: 20px; + padding: 0 15px 0 0; + border: 0 !important; + border-radius: 0; + color: var(--mr-text-muted); + background: transparent !important; + box-shadow: none !important; + font: inherit; + font-weight: 700; + line-height: 1.1; + cursor: pointer; + appearance: none; + -webkit-appearance: none; +} + +#model-resolver-modal .mr-missing-type-filter-button::after { + content: ""; + position: absolute; + right: 1px; + width: 0; + height: 0; + border-right: 4px solid transparent; + border-left: 4px solid transparent; + border-top: 5px solid currentColor; + opacity: 0.72; +} + +#model-resolver-modal .mr-missing-type-filter-button:hover, +#model-resolver-modal .mr-missing-type-filter-button:focus-visible, +#model-resolver-modal .mr-missing-type-filter-button.is-filtered { + color: var(--mr-text); + outline: none; +} + +.mr-missing-type-filter-chip { + min-width: 0; + max-width: 72px; + color: var(--mr-text-muted); + font-size: 10px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.mr-missing-type-filter-menu { + position: absolute; + top: calc(100% + 5px); + left: 50%; + z-index: 20; + display: grid; + gap: 0; + min-width: 170px; + max-height: 320px; + padding: 5px; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 6px; + background: #121212; + box-shadow: 0 18px 36px rgba(0, 0, 0, 0.38); + overflow-y: auto; + transform: translateX(-50%); +} + +.mr-missing-type-filter-menu[hidden] { + display: none; +} + +#model-resolver-modal .mr-missing-type-filter-option { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + min-height: 26px; + padding: 5px 7px; + border: 0 !important; + border-radius: 4px; + color: var(--mr-text-secondary); + background: transparent !important; + box-shadow: none !important; + font-size: 12px; + font-weight: 500; + line-height: 1.1; + text-align: left; + cursor: pointer; + appearance: none; + -webkit-appearance: none; +} + +#model-resolver-modal .mr-missing-type-filter-option:hover, +#model-resolver-modal .mr-missing-type-filter-option:focus-visible { + color: var(--mr-text); + background: rgba(255, 255, 255, 0.055) !important; + outline: none; +} + +#model-resolver-modal .mr-missing-type-filter-option.is-active { + color: var(--mr-text); + background: rgba(255, 255, 255, 0.075) !important; +} + +#model-resolver-modal .mr-missing-type-filter-option span { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +#model-resolver-modal .mr-missing-type-filter-option-label.mr-type-chip { + display: inline-flex; + align-items: center; + max-width: 112px; + padding: 2px 7px; + border-radius: 999px; + color: var(--mr-type-text); + font-size: 9px; + font-weight: 800; + letter-spacing: 0.02em; + line-height: normal; + text-transform: uppercase; +} + +#model-resolver-modal .mr-missing-type-filter-option strong { + min-width: 34px; + color: var(--mr-text-muted); + font-size: 11px; + font-weight: 700; + text-align: right; +} diff --git a/web/resolver/resolver_dialog.js b/web/resolver/resolver_dialog.js index c306bd1..0043ade 100644 --- a/web/resolver/resolver_dialog.js +++ b/web/resolver/resolver_dialog.js @@ -76,6 +76,8 @@ export class ResolverManagerDialog extends ComfyDialog { this.showAutoDownloadModelsStorageKey = "model_resolver_show_auto_download_models"; this.localMatchAlternativesCollapsedStorageKey = "model_resolver_local_match_alternatives_collapsed"; this.showResolvedModels = safeStorage.getItem(this.showResolvedModelsStorageKey) === '1'; + this.missingModelsTypeFilter = 'all'; + this.missingModelsTypeFilterMenuOpen = false; const storedShowAutoDownload = safeStorage.getItem(this.showAutoDownloadModelsStorageKey); this.showAutoDownloadModels = storedShowAutoDownload === null ? true diff --git a/web/resolver/views/missing_browser_methods.js b/web/resolver/views/missing_browser_methods.js index 8c0bf04..a53fa4b 100644 --- a/web/resolver/views/missing_browser_methods.js +++ b/web/resolver/views/missing_browser_methods.js @@ -539,6 +539,11 @@ export const missingBrowserMethods = { const resolvedCount = Number(options.resolvedCount || 0); const rawMissingCount = Number(options.rawMissingCount ?? totalMissing); const missingCount = Number(options.missingCount ?? rawMissingCount); + const typeFilterOptions = Array.isArray(options.typeFilterOptions) ? options.typeFilterOptions : []; + const activeTypeFilter = String(options.activeTypeFilter || 'all'); + const activeTypeOption = typeFilterOptions.find(option => option.value === activeTypeFilter); + const activeTypeLabel = activeTypeOption?.label || 'All'; + const typeFilterMenuOpen = Boolean(this.missingModelsTypeFilterMenuOpen); const resolvedToggleCount = this.showResolvedModels ? resolvedCount : hiddenResolvedCount; const autoDownloadToggleCount = this.showAutoDownloadModels ? autoDownloadCount : hiddenAutoDownloadCount; const stats = this.getMissingModelSummaryStats(missingModels); @@ -558,9 +563,24 @@ export const missingBrowserMethods = { : ''; const listStyle = `--mr-missing-model-col:${listLayout.modelPx}px;--mr-missing-type-col:${listLayout.typePx}px;`; const browserStyle = `${listStyle}${splitStyle}`; - const titleText = this.showResolvedModels && resolvedCount > 0 + const typeFilterTotal = Number(typeFilterOptions.find(option => option.value === 'all')?.count || totalMissing); + const titleText = activeTypeFilter !== 'all' + ? `${totalMissing} shown of ${typeFilterTotal}` + : this.showResolvedModels && resolvedCount > 0 ? `${totalMissing} shown (${missingCount} missing / ${resolvedCount} resolved)` : `${totalMissing} missing model${totalMissing === 1 ? '' : 's'}`; + const typeFilterMenuOptions = typeFilterOptions.map((option) => { + const isActive = option.value === activeTypeFilter; + const labelClass = option.colorClass + ? `mr-missing-type-filter-option-label ${option.colorClass}` + : 'mr-missing-type-filter-option-label'; + return ` + + `; + }).join(''); let html = `
@@ -598,7 +618,17 @@ export const missingBrowserMethods = { # Missing Model - Type + + + + + ${typeFilterMenuOptions} + + + Best Local Match Match Sources @@ -666,6 +696,46 @@ export const missingBrowserMethods = { wireMissingModelsBrowser(container, data, sortedMissingModels) { this.wireMissingBrowserSplitter(container); + const browser = container.querySelector('.mr-missing-browser'); + const typeFilterToggle = browser?.querySelector('[data-missing-type-filter-toggle]'); + const typeFilterMenu = browser?.querySelector('.mr-missing-type-filter-menu'); + const setTypeFilterMenuOpen = (open) => { + this.missingModelsTypeFilterMenuOpen = Boolean(open); + if (typeFilterMenu) typeFilterMenu.hidden = !this.missingModelsTypeFilterMenuOpen; + if (typeFilterToggle) { + typeFilterToggle.setAttribute('aria-expanded', this.missingModelsTypeFilterMenuOpen ? 'true' : 'false'); + } + }; + + typeFilterToggle?.addEventListener('click', (event) => { + event.preventDefault(); + event.stopPropagation(); + setTypeFilterMenuOpen(!this.missingModelsTypeFilterMenuOpen); + }); + + browser?.querySelectorAll('[data-missing-type-filter-option]').forEach((option) => { + option.addEventListener('click', (event) => { + event.preventDefault(); + event.stopPropagation(); + this.missingModelsTypeFilter = option.dataset.missingTypeFilterOption || 'all'; + this.missingModelsTypeFilterMenuOpen = false; + this.displayMissingModels(container, data); + }); + }); + + browser?.addEventListener('click', (event) => { + if (!this.missingModelsTypeFilterMenuOpen) return; + if (event.target instanceof Element && event.target.closest('.mr-missing-type-filter-wrap')) return; + setTypeFilterMenuOpen(false); + }); + + browser?.addEventListener('keydown', (event) => { + if (event.key !== 'Escape' || !this.missingModelsTypeFilterMenuOpen) return; + event.preventDefault(); + setTypeFilterMenuOpen(false); + typeFilterToggle?.focus(); + }); + const refreshBtn = container.querySelector('#mr-refresh-missing-analysis'); if (refreshBtn && refreshBtn.dataset.mlRefreshBound !== 'true') { refreshBtn.dataset.mlRefreshBound = 'true'; @@ -1838,9 +1908,40 @@ export const missingBrowserMethods = { count + (this.isAutoDownloadModel(missing) ? 1 : 0) ), 0); const hiddenAutoDownloadCount = this.showAutoDownloadModels ? 0 : autoDownloadCount; - const visibleMissingModels = this.showAutoDownloadModels + const typeFilterSourceModels = this.showAutoDownloadModels ? resolvedFilteredModels : resolvedFilteredModels.filter(missing => !this.isAutoDownloadModel(missing)); + const getTypeFilterValue = missing => String(missing?.category || 'unknown').trim().toLowerCase() || 'unknown'; + const typeFilterCounts = new Map(); + const typeFilterLabels = new Map(); + typeFilterSourceModels.forEach((missing) => { + const value = getTypeFilterValue(missing); + typeFilterCounts.set(value, (typeFilterCounts.get(value) || 0) + 1); + if (!typeFilterLabels.has(value)) { + typeFilterLabels.set( + value, + missing.category ? this.getCategoryDisplayName(missing.category) : 'Unknown' + ); + } + }); + let activeTypeFilter = String(this.missingModelsTypeFilter || 'all').toLowerCase(); + if (activeTypeFilter !== 'all' && !typeFilterCounts.has(activeTypeFilter)) { + activeTypeFilter = 'all'; + this.missingModelsTypeFilter = 'all'; + this.missingModelsTypeFilterMenuOpen = false; + } + const typeFilterOptions = [ + { value: 'all', label: 'All', count: typeFilterSourceModels.length }, + ...Array.from(typeFilterCounts, ([value, count]) => ({ + value, + label: typeFilterLabels.get(value) || value, + count, + colorClass: this.getModelTypeColorClass(value) + })).sort((left, right) => left.label.localeCompare(right.label)) + ]; + const visibleMissingModels = activeTypeFilter === 'all' + ? typeFilterSourceModels + : typeFilterSourceModels.filter(missing => getTypeFilterValue(missing) === activeTypeFilter); this.missingModels = visibleMissingModels; this.syncBatchSelectionForMissingModels(visibleMissingModels); @@ -1987,6 +2088,8 @@ export const missingBrowserMethods = { resolvedCount, rawMissingCount, missingCount: unresolvedMissingCount, + typeFilterOptions, + activeTypeFilter, } ); this.wireMissingModelsBrowser(container, data, sortedMissingModels); diff --git a/web/utils/css_loader.js b/web/utils/css_loader.js index a214ede..73dc566 100644 --- a/web/utils/css_loader.js +++ b/web/utils/css_loader.js @@ -20,6 +20,7 @@ export function loadAllStyles() { './css/css-variables.css', './css/resolver-shell.css', './css/resolver-main.css', + './resolver/missing_type_filter.css', ]; cssFiles.forEach(file => {