mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-05-17 10:37:35 -03:00
feat(ui): auto-detect HIGH/LOW badges and auto-tag filters (#918)
- Backend auto-tag extraction service: detect HIGH/LOW (Wan-only), I2V/T2V/TI2V, Lightning/Turbo from filename, base_model, and CivitAI version name - HIGH/LOW badge in card footer (inline before version name), color-coded: blue for HIGH, teal for LOW; abbreviated to H/L in medium/compact density - Auto-tag filter panel (I2V, T2V, TI2V, Lightning, Turbo) with tri-state include/exclude filtering - Full filter pipeline: FilterCriteria → ModelFilterSet → baseModelApi params - AUTO_TAG_GROUPS exported for frontend use - 19 unit tests for auto-tag extraction edge cases
This commit is contained in:
@@ -978,6 +978,16 @@ export class BaseModelApiClient {
|
||||
});
|
||||
}
|
||||
|
||||
if (pageState.filters.autoTags && Object.keys(pageState.filters.autoTags).length > 0) {
|
||||
Object.entries(pageState.filters.autoTags).forEach(([tag, state]) => {
|
||||
if (state === 'include') {
|
||||
params.append('auto_tag_include', tag);
|
||||
} else if (state === 'exclude') {
|
||||
params.append('auto_tag_exclude', tag);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (pageState.filters.baseModel && pageState.filters.baseModel.length > 0) {
|
||||
// Check for empty wildcard marker - if present, no models should match
|
||||
const EMPTY_WILDCARD_MARKER = '__EMPTY_WILDCARD_RESULT__';
|
||||
|
||||
@@ -644,8 +644,23 @@ export function createModelCard(model, modelType) {
|
||||
<div class="card-footer">
|
||||
<div class="model-info">
|
||||
<span class="model-name" title="${getDisplayName(model).replace(/"/g, '"')}">${getDisplayName(model)}</span>
|
||||
<div>
|
||||
${model.civitai?.name ? `<span class="version-name civitai-version">${model.civitai.name}</span>` : ''}
|
||||
<div class="version-row">
|
||||
${(() => {
|
||||
const autoTags = model.auto_tags || [];
|
||||
const hlTags = autoTags.filter(t => t === 'HIGH' || t === 'LOW');
|
||||
const hasVersionName = model.civitai?.name;
|
||||
if (!hlTags.length && !hasVersionName) return '';
|
||||
const density = state.global.settings.display_density || 'default';
|
||||
const shortLabels = density === 'medium' || density === 'compact';
|
||||
const badges = hlTags.map(t => {
|
||||
const cls = t === 'HIGH' ? 'hl-badge hl-badge--high' : 'hl-badge hl-badge--low';
|
||||
const label = shortLabels ? (t === 'HIGH' ? 'H' : 'L') : t;
|
||||
const titleAttr = shortLabels ? ` title="${t}"` : '';
|
||||
return `<span class="${cls}"${titleAttr}>${label}</span>`;
|
||||
}).join('');
|
||||
const versionHtml = hasVersionName ? `<span class="version-name civitai-version">${model.civitai.name}</span>` : '';
|
||||
return `<span class="badge-version-unit">${badges}${versionHtml}</span>`;
|
||||
})()}
|
||||
${hasUsageCount ? `<span class="version-name" title="${translate('modelCard.usage.timesUsed', {}, 'Times used')}">${model.usage_count}×</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -70,6 +70,9 @@ export class FilterManager {
|
||||
// Initialize tag logic toggle
|
||||
this.initializeTagLogicToggle();
|
||||
|
||||
// Create auto-tag filter section (I2V, T2V, TI2V, Lightning, Turbo)
|
||||
this.createAutoTagFilters();
|
||||
|
||||
// Add click handler for filter button
|
||||
if (this.filterButton) {
|
||||
this.filterButton.addEventListener('click', () => {
|
||||
@@ -480,6 +483,58 @@ export class FilterManager {
|
||||
}
|
||||
}
|
||||
|
||||
AUTO_TAG_FILTER_TAGS = ['I2V', 'T2V', 'TI2V', 'Lightning', 'Turbo'];
|
||||
|
||||
createAutoTagFilters() {
|
||||
const container = document.getElementById('autoTagFilterTags');
|
||||
if (container) return;
|
||||
|
||||
const modelTypeSection = document.getElementById('modelTypeTags')?.closest('.filter-section');
|
||||
if (!modelTypeSection) return;
|
||||
|
||||
const section = document.createElement('div');
|
||||
section.className = 'filter-section';
|
||||
section.innerHTML = `
|
||||
<h4>${translate('header.filter.autoTags', {}, 'Auto Tags')}</h4>
|
||||
<div class="filter-tags" id="autoTagFilterTags"></div>
|
||||
`;
|
||||
modelTypeSection.parentNode.insertBefore(section, modelTypeSection.nextSibling);
|
||||
|
||||
const tagsContainer = document.getElementById('autoTagFilterTags');
|
||||
this.AUTO_TAG_FILTER_TAGS.forEach(tag => {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'filter-tag auto-tag-filter';
|
||||
el.dataset.autoTag = tag;
|
||||
el.textContent = tag;
|
||||
|
||||
// Restore previous state
|
||||
const state = (this.filters.autoTags && this.filters.autoTags[tag]) || 'none';
|
||||
this._applyTriState(el, state);
|
||||
|
||||
el.addEventListener('click', async () => {
|
||||
const current = (this.filters.autoTags && this.filters.autoTags[tag]) || 'none';
|
||||
const next = current === 'none' ? 'include' : current === 'include' ? 'exclude' : 'none';
|
||||
if (!this.filters.autoTags) this.filters.autoTags = {};
|
||||
if (next === 'none') {
|
||||
delete this.filters.autoTags[tag];
|
||||
} else {
|
||||
this.filters.autoTags[tag] = next;
|
||||
}
|
||||
this._applyTriState(el, next);
|
||||
this.updateActiveFiltersCount();
|
||||
await this.applyFilters(false);
|
||||
});
|
||||
|
||||
tagsContainer.appendChild(el);
|
||||
});
|
||||
}
|
||||
|
||||
_applyTriState(el, state) {
|
||||
el.classList.remove('active', 'exclude');
|
||||
if (state === 'include') el.classList.add('active');
|
||||
else if (state === 'exclude') el.classList.add('exclude');
|
||||
}
|
||||
|
||||
toggleFilterPanel() {
|
||||
if (this.filterPanel) {
|
||||
const isHidden = this.filterPanel.classList.contains('hidden');
|
||||
@@ -540,6 +595,13 @@ export class FilterManager {
|
||||
this.updateLicenseSelections();
|
||||
}
|
||||
this.updateModelTypeSelections();
|
||||
|
||||
const autoTagEls = document.querySelectorAll('.auto-tag-filter');
|
||||
autoTagEls.forEach(el => {
|
||||
const tag = el.dataset.autoTag;
|
||||
const state = (this.filters.autoTags && this.filters.autoTags[tag]) || 'none';
|
||||
this._applyTriState(el, state);
|
||||
});
|
||||
}
|
||||
|
||||
updateModelTypeSelections() {
|
||||
@@ -556,11 +618,12 @@ export class FilterManager {
|
||||
|
||||
updateActiveFiltersCount() {
|
||||
const tagFilterCount = this.filters.tags ? Object.keys(this.filters.tags).length : 0;
|
||||
const autoTagFilterCount = this.filters.autoTags ? Object.keys(this.filters.autoTags).length : 0;
|
||||
const licenseFilterCount = this.filters.license ? Object.keys(this.filters.license).length : 0;
|
||||
const modelTypeFilterCount = this.filters.modelTypes.length;
|
||||
// Exclude EMPTY_WILDCARD_MARKER from base model count
|
||||
const baseModelCount = this.filters.baseModel.filter(m => m !== EMPTY_WILDCARD_MARKER).length;
|
||||
const totalActiveFilters = baseModelCount + tagFilterCount + licenseFilterCount + modelTypeFilterCount;
|
||||
const totalActiveFilters = baseModelCount + tagFilterCount + autoTagFilterCount + licenseFilterCount + modelTypeFilterCount;
|
||||
|
||||
if (this.activeFiltersCount) {
|
||||
if (totalActiveFilters > 0) {
|
||||
@@ -652,6 +715,7 @@ export class FilterManager {
|
||||
...this.filters,
|
||||
baseModel: [],
|
||||
tags: {},
|
||||
autoTags: {},
|
||||
license: {},
|
||||
modelTypes: [],
|
||||
tagLogic: 'any'
|
||||
@@ -721,6 +785,7 @@ export class FilterManager {
|
||||
|
||||
hasActiveFilters() {
|
||||
const tagCount = this.filters.tags ? Object.keys(this.filters.tags).length : 0;
|
||||
const autoTagCount = this.filters.autoTags ? Object.keys(this.filters.autoTags).length : 0;
|
||||
const licenseCount = this.filters.license ? Object.keys(this.filters.license).length : 0;
|
||||
const modelTypeCount = this.filters.modelTypes.length;
|
||||
// Exclude EMPTY_WILDCARD_MARKER from base model count
|
||||
@@ -728,6 +793,7 @@ export class FilterManager {
|
||||
return (
|
||||
baseModelCount > 0 ||
|
||||
tagCount > 0 ||
|
||||
autoTagCount > 0 ||
|
||||
licenseCount > 0 ||
|
||||
modelTypeCount > 0
|
||||
);
|
||||
@@ -739,6 +805,7 @@ export class FilterManager {
|
||||
...source,
|
||||
baseModel: Array.isArray(source.baseModel) ? [...source.baseModel] : [],
|
||||
tags: this.normalizeTagFilters(source.tags),
|
||||
autoTags: this.normalizeTagFilters(source.autoTags),
|
||||
license: this.shouldShowLicenseFilters() ? this.normalizeLicenseFilters(source.license) : {},
|
||||
modelTypes: this.normalizeModelTypeFilters(source.modelTypes),
|
||||
tagLogic: source.tagLogic || 'any'
|
||||
@@ -822,6 +889,7 @@ export class FilterManager {
|
||||
...this.filters,
|
||||
baseModel: [...(this.filters.baseModel || [])],
|
||||
tags: { ...(this.filters.tags || {}) },
|
||||
autoTags: { ...(this.filters.autoTags || {}) },
|
||||
license: { ...(this.filters.license || {}) },
|
||||
modelTypes: [...(this.filters.modelTypes || [])],
|
||||
tagLogic: this.filters.tagLogic || 'any'
|
||||
|
||||
@@ -500,6 +500,18 @@ export function clearDynamicBaseModels() {
|
||||
dynamicBaseModelsTimestamp = null;
|
||||
}
|
||||
|
||||
export const AUTO_TAG_GROUPS = {
|
||||
mode: new Set(['HIGH', 'LOW']),
|
||||
video: new Set(['I2V', 'T2V', 'TI2V']),
|
||||
speed: new Set(['Lightning', 'Turbo']),
|
||||
};
|
||||
|
||||
export const AUTO_TAG_GROUP_LABELS = {
|
||||
mode: 'High / Low',
|
||||
video: 'I2V / T2V / TI2V',
|
||||
speed: 'Lightning / Turbo',
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if dynamic base models cache is valid
|
||||
* @returns {boolean}
|
||||
|
||||
Reference in New Issue
Block a user