feat(recipes): add lora availability filter to recipe filter panel

This commit is contained in:
Will Miao
2026-08-24 16:59:42 +08:00
parent 20f66a4fe1
commit 6f5c444ec5
20 changed files with 763 additions and 12 deletions
+5
View File
@@ -174,6 +174,11 @@ export async function fetchRecipesPage(page = 1, pageSize = 100) {
}
});
}
// Add LoRA availability filter (no statuses selected = no filtering)
if (pageState.filters?.loraAvailability && pageState.filters.loraAvailability.length > 0) {
params.append('lora_availability', pageState.filters.loraAvailability.join(','));
}
}
// Fetch recipes
+86 -2
View File
@@ -7,6 +7,10 @@ import { MODEL_TYPE_DISPLAY_NAMES } from '../utils/constants.js';
import { translate } from '../utils/i18nHelpers.js';
import { FilterPresetManager, EMPTY_WILDCARD_MARKER } from './FilterPresetManager.js';
// LoRA availability statuses available on the recipes page. No statuses
// selected (the default) means no filtering.
const LORA_AVAILABILITY_STATUSES = ['ready', 'missing', 'deleted'];
export class FilterManager {
constructor(options = {}) {
this.options = {
@@ -74,6 +78,11 @@ export class FilterManager {
this.initializeLicenseFilters();
}
// Add click handlers for LoRA availability tags (recipes page only)
if (this.shouldShowLoraAvailabilityFilter()) {
this.initializeLoraAvailabilityFilters();
}
// Initialize tag logic toggle
this.initializeTagLogicToggle();
@@ -421,6 +430,42 @@ export class FilterManager {
});
}
initializeLoraAvailabilityFilters() {
const availabilityTags = document.querySelectorAll('.lora-availability-tag');
availabilityTags.forEach(tag => {
tag.addEventListener('click', async () => {
const status = tag.dataset.availability;
const selected = this.filters.loraAvailability || [];
if (selected.includes(status)) {
this.filters.loraAvailability = selected.filter(value => value !== status);
tag.classList.remove('active');
} else {
this.filters.loraAvailability = [...selected, status];
tag.classList.add('active');
}
this.updateActiveFiltersCount();
await this.applyFilters(false);
});
});
// Update selections based on stored filters
this.updateLoraAvailabilitySelections();
}
updateLoraAvailabilitySelections() {
const availabilityTags = document.querySelectorAll('.lora-availability-tag');
const selected = this.filters.loraAvailability || [];
availabilityTags.forEach(tag => {
if (selected.includes(tag.dataset.availability)) {
tag.classList.add('active');
} else {
tag.classList.remove('active');
}
});
}
createBaseModelTags() {
const baseModelTagsContainer = document.getElementById('baseModelTags');
if (!baseModelTagsContainer) return;
@@ -681,6 +726,11 @@ export class FilterManager {
}
this.updateModelTypeSelections();
// Update LoRA availability tags if visible on this page
if (this.shouldShowLoraAvailabilityFilter()) {
this.updateLoraAvailabilitySelections();
}
const autoTagEls = document.querySelectorAll('.auto-tag-filter');
autoTagEls.forEach(el => {
const tag = el.dataset.autoTag;
@@ -708,7 +758,9 @@ export class FilterManager {
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 + autoTagFilterCount + licenseFilterCount + modelTypeFilterCount;
// Active when at least one availability status is deselected
const loraAvailabilityCount = this.filters.loraAvailability?.length ?? 0;
const totalActiveFilters = baseModelCount + tagFilterCount + autoTagFilterCount + licenseFilterCount + modelTypeFilterCount + loraAvailabilityCount;
if (this.activeFiltersCount) {
if (totalActiveFilters > 0) {
@@ -805,6 +857,7 @@ export class FilterManager {
autoTags: {},
license: {},
modelTypes: [],
loraAvailability: [],
tagLogic: 'any'
});
@@ -891,12 +944,14 @@ export class FilterManager {
const modelTypeCount = 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 loraAvailabilityCount = this.filters.loraAvailability?.length ?? 0;
return (
baseModelCount > 0 ||
tagCount > 0 ||
autoTagCount > 0 ||
licenseCount > 0 ||
modelTypeCount > 0
modelTypeCount > 0 ||
loraAvailabilityCount > 0
);
}
@@ -909,6 +964,7 @@ export class FilterManager {
autoTags: this.normalizeTagFilters(source.autoTags),
license: this.shouldShowLicenseFilters() ? this.normalizeLicenseFilters(source.license) : {},
modelTypes: this.normalizeModelTypeFilters(source.modelTypes),
loraAvailability: this.normalizeLoraAvailabilityFilters(source.loraAvailability),
tagLogic: source.tagLogic || 'any'
};
}
@@ -917,6 +973,33 @@ export class FilterManager {
return this.currentPage !== 'recipes';
}
shouldShowLoraAvailabilityFilter() {
return this.currentPage === 'recipes';
}
normalizeLoraAvailabilityFilters(loraAvailability) {
// Default to no statuses selected (= no filtering)
if (!Array.isArray(loraAvailability)) {
return [];
}
const seen = new Set();
return loraAvailability.reduce((acc, status) => {
if (typeof status !== 'string') {
return acc;
}
const normalized = status.trim().toLowerCase();
if (!LORA_AVAILABILITY_STATUSES.includes(normalized) || seen.has(normalized)) {
return acc;
}
seen.add(normalized);
acc.push(normalized);
return acc;
}, []);
}
normalizeTagFilters(tagFilters) {
if (!tagFilters) {
return {};
@@ -994,6 +1077,7 @@ export class FilterManager {
autoTags: { ...(this.filters.autoTags || {}) },
license: { ...(this.filters.license || {}) },
modelTypes: [...(this.filters.modelTypes || [])],
loraAvailability: [...(this.filters.loraAvailability || [])],
tagLogic: this.filters.tagLogic || 'any',
search: pageState?.filters?.search ?? ''
};
+1
View File
@@ -148,6 +148,7 @@ export const state = {
tags: {},
license: {},
modelTypes: [],
loraAvailability: [],
search: ''
},
pageSize: 20,