mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 07:05:43 -03:00
Refactor API routes and enhance recipe and filter management
- Removed the handle_get_recipes method from ApiRoutes to streamline the API structure. - Updated RecipeRoutes to include logging for recipe retrieval requests and improved filter management. - Consolidated filter management logic in FilterManager to support both recipes and loras, enhancing code reusability. - Deleted obsolete LoraSearchManager and RecipeSearchManager classes to simplify the search functionality. - Improved infinite scroll implementation for both recipes and loras, ensuring consistent loading behavior across pages.
This commit is contained in:
@@ -1,11 +1,17 @@
|
||||
import { BASE_MODELS, BASE_MODEL_CLASSES } from '../utils/constants.js';
|
||||
import { state, getCurrentPageState } from '../state/index.js';
|
||||
import { showToast, updatePanelPositions } from '../utils/uiHelpers.js';
|
||||
import { resetAndReload } from '../api/loraApi.js';
|
||||
import { loadMoreLoras } from '../api/loraApi.js';
|
||||
|
||||
export class FilterManager {
|
||||
constructor() {
|
||||
constructor(options = {}) {
|
||||
this.options = {
|
||||
...options
|
||||
};
|
||||
|
||||
this.currentPage = options.page || document.body.dataset.page || 'loras';
|
||||
const pageState = getCurrentPageState();
|
||||
|
||||
this.filters = pageState.filters || {
|
||||
baseModel: [],
|
||||
tags: []
|
||||
@@ -17,11 +23,18 @@ export class FilterManager {
|
||||
this.tagsLoaded = false;
|
||||
|
||||
this.initialize();
|
||||
|
||||
// Store this instance in the state
|
||||
if (pageState) {
|
||||
pageState.filterManager = this;
|
||||
}
|
||||
}
|
||||
|
||||
initialize() {
|
||||
// Create base model filter tags
|
||||
this.createBaseModelTags();
|
||||
// Create base model filter tags if they exist
|
||||
if (document.getElementById('baseModelTags')) {
|
||||
this.createBaseModelTags();
|
||||
}
|
||||
|
||||
// Add click handler for filter button
|
||||
if (this.filterButton) {
|
||||
@@ -32,7 +45,7 @@ export class FilterManager {
|
||||
|
||||
// Close filter panel when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!this.filterPanel.contains(e.target) &&
|
||||
if (this.filterPanel && !this.filterPanel.contains(e.target) &&
|
||||
e.target !== this.filterButton &&
|
||||
!this.filterButton.contains(e.target) &&
|
||||
!this.filterPanel.classList.contains('hidden')) {
|
||||
@@ -48,15 +61,20 @@ export class FilterManager {
|
||||
try {
|
||||
// Show loading state
|
||||
const tagsContainer = document.getElementById('modelTagsFilter');
|
||||
if (tagsContainer) {
|
||||
tagsContainer.innerHTML = '<div class="tags-loading">Loading tags...</div>';
|
||||
if (!tagsContainer) return;
|
||||
|
||||
tagsContainer.innerHTML = '<div class="tags-loading">Loading tags...</div>';
|
||||
|
||||
// Determine the API endpoint based on the page type
|
||||
let tagsEndpoint = '/api/top-tags?limit=20';
|
||||
if (this.currentPage === 'recipes') {
|
||||
tagsEndpoint = '/api/recipes/top-tags?limit=20';
|
||||
}
|
||||
|
||||
const response = await fetch('/api/top-tags?limit=20');
|
||||
const response = await fetch(tagsEndpoint);
|
||||
if (!response.ok) throw new Error('Failed to fetch tags');
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Top tags:', data);
|
||||
if (data.success && data.tags) {
|
||||
this.createTagFilterElements(data.tags);
|
||||
|
||||
@@ -81,14 +99,13 @@ export class FilterManager {
|
||||
tagsContainer.innerHTML = '';
|
||||
|
||||
if (!tags.length) {
|
||||
tagsContainer.innerHTML = '<div class="no-tags">No tags available</div>';
|
||||
tagsContainer.innerHTML = `<div class="no-tags">No ${this.currentPage === 'recipes' ? 'recipe ' : ''}tags available</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
tags.forEach(tag => {
|
||||
const tagEl = document.createElement('div');
|
||||
tagEl.className = 'filter-tag tag-filter';
|
||||
// {tag: "name", count: number}
|
||||
const tagName = tag.tag;
|
||||
tagEl.dataset.tag = tagName;
|
||||
tagEl.innerHTML = `${tagName} <span class="tag-count">${tag.count}</span>`;
|
||||
@@ -119,34 +136,80 @@ export class FilterManager {
|
||||
const baseModelTagsContainer = document.getElementById('baseModelTags');
|
||||
if (!baseModelTagsContainer) return;
|
||||
|
||||
baseModelTagsContainer.innerHTML = '';
|
||||
|
||||
Object.entries(BASE_MODELS).forEach(([key, value]) => {
|
||||
const tag = document.createElement('div');
|
||||
tag.className = `filter-tag base-model-tag ${BASE_MODEL_CLASSES[value]}`;
|
||||
tag.dataset.baseModel = value;
|
||||
tag.innerHTML = value;
|
||||
if (this.currentPage === 'loras') {
|
||||
// Use predefined base models for loras page
|
||||
baseModelTagsContainer.innerHTML = '';
|
||||
|
||||
// Add click handler to toggle selection and automatically apply
|
||||
tag.addEventListener('click', async () => {
|
||||
tag.classList.toggle('active');
|
||||
Object.entries(BASE_MODELS).forEach(([key, value]) => {
|
||||
const tag = document.createElement('div');
|
||||
tag.className = `filter-tag base-model-tag ${BASE_MODEL_CLASSES[value]}`;
|
||||
tag.dataset.baseModel = value;
|
||||
tag.innerHTML = value;
|
||||
|
||||
if (tag.classList.contains('active')) {
|
||||
if (!this.filters.baseModel.includes(value)) {
|
||||
this.filters.baseModel.push(value);
|
||||
// Add click handler to toggle selection and automatically apply
|
||||
tag.addEventListener('click', async () => {
|
||||
tag.classList.toggle('active');
|
||||
|
||||
if (tag.classList.contains('active')) {
|
||||
if (!this.filters.baseModel.includes(value)) {
|
||||
this.filters.baseModel.push(value);
|
||||
}
|
||||
} else {
|
||||
this.filters.baseModel = this.filters.baseModel.filter(model => model !== value);
|
||||
}
|
||||
} else {
|
||||
this.filters.baseModel = this.filters.baseModel.filter(model => model !== value);
|
||||
}
|
||||
|
||||
this.updateActiveFiltersCount();
|
||||
|
||||
// Auto-apply filter when tag is clicked
|
||||
await this.applyFilters(false);
|
||||
});
|
||||
|
||||
this.updateActiveFiltersCount();
|
||||
|
||||
// Auto-apply filter when tag is clicked
|
||||
await this.applyFilters(false);
|
||||
baseModelTagsContainer.appendChild(tag);
|
||||
});
|
||||
|
||||
baseModelTagsContainer.appendChild(tag);
|
||||
});
|
||||
} else if (this.currentPage === 'recipes') {
|
||||
// Fetch base models for recipes
|
||||
fetch('/api/recipes/base-models')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success && data.base_models) {
|
||||
baseModelTagsContainer.innerHTML = '';
|
||||
|
||||
data.base_models.forEach(model => {
|
||||
const tag = document.createElement('div');
|
||||
tag.className = `filter-tag base-model-tag`;
|
||||
tag.dataset.baseModel = model.name;
|
||||
tag.innerHTML = `${model.name} <span class="tag-count">${model.count}</span>`;
|
||||
|
||||
// Add click handler to toggle selection and automatically apply
|
||||
tag.addEventListener('click', async () => {
|
||||
tag.classList.toggle('active');
|
||||
|
||||
if (tag.classList.contains('active')) {
|
||||
if (!this.filters.baseModel.includes(model.name)) {
|
||||
this.filters.baseModel.push(model.name);
|
||||
}
|
||||
} else {
|
||||
this.filters.baseModel = this.filters.baseModel.filter(m => m !== model.name);
|
||||
}
|
||||
|
||||
this.updateActiveFiltersCount();
|
||||
|
||||
// Auto-apply filter when tag is clicked
|
||||
await this.applyFilters(false);
|
||||
});
|
||||
|
||||
baseModelTagsContainer.appendChild(tag);
|
||||
});
|
||||
|
||||
// Update selections based on stored filters
|
||||
this.updateTagSelections();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching base models:', error);
|
||||
baseModelTagsContainer.innerHTML = '<div class="tags-error">Failed to load base models</div>';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
toggleFilterPanel() {
|
||||
@@ -172,8 +235,12 @@ export class FilterManager {
|
||||
}
|
||||
|
||||
closeFilterPanel() {
|
||||
this.filterPanel.classList.add('hidden');
|
||||
this.filterButton.classList.remove('active');
|
||||
if (this.filterPanel) {
|
||||
this.filterPanel.classList.add('hidden');
|
||||
}
|
||||
if (this.filterButton) {
|
||||
this.filterButton.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
updateTagSelections() {
|
||||
@@ -203,24 +270,35 @@ export class FilterManager {
|
||||
updateActiveFiltersCount() {
|
||||
const totalActiveFilters = this.filters.baseModel.length + this.filters.tags.length;
|
||||
|
||||
if (totalActiveFilters > 0) {
|
||||
this.activeFiltersCount.textContent = totalActiveFilters;
|
||||
this.activeFiltersCount.style.display = 'inline-flex';
|
||||
} else {
|
||||
this.activeFiltersCount.style.display = 'none';
|
||||
if (this.activeFiltersCount) {
|
||||
if (totalActiveFilters > 0) {
|
||||
this.activeFiltersCount.textContent = totalActiveFilters;
|
||||
this.activeFiltersCount.style.display = 'inline-flex';
|
||||
} else {
|
||||
this.activeFiltersCount.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async applyFilters(showToastNotification = true) {
|
||||
const pageState = getCurrentPageState();
|
||||
const storageKey = `${this.currentPage}_filters`;
|
||||
|
||||
// Save filters to localStorage
|
||||
localStorage.setItem('loraFilters', JSON.stringify(this.filters));
|
||||
localStorage.setItem(storageKey, JSON.stringify(this.filters));
|
||||
|
||||
// Update state with current filters
|
||||
const pageState = getCurrentPageState();
|
||||
pageState.filters = { ...this.filters };
|
||||
|
||||
// Reload loras with filters applied
|
||||
await resetAndReload();
|
||||
// Call the appropriate manager's load method based on page type
|
||||
if (this.currentPage === 'recipes' && window.recipeManager) {
|
||||
await window.recipeManager.loadRecipes(true);
|
||||
} else if (this.currentPage === 'loras') {
|
||||
// For loras page, reset the page and reload
|
||||
await loadMoreLoras(true, true);
|
||||
} else if (this.currentPage === 'checkpoints' && window.checkpointManager) {
|
||||
await window.checkpointManager.loadCheckpoints(true);
|
||||
}
|
||||
|
||||
// Update filter button to show active state
|
||||
if (this.hasActiveFilters()) {
|
||||
@@ -264,15 +342,28 @@ export class FilterManager {
|
||||
this.updateActiveFiltersCount();
|
||||
|
||||
// Remove from localStorage
|
||||
localStorage.removeItem('loraFilters');
|
||||
const storageKey = `${this.currentPage}_filters`;
|
||||
localStorage.removeItem(storageKey);
|
||||
|
||||
// Update UI and reload data
|
||||
// Update UI
|
||||
this.filterButton.classList.remove('active');
|
||||
await resetAndReload();
|
||||
|
||||
// Reload data using the appropriate method for the current page
|
||||
if (this.currentPage === 'recipes' && window.recipeManager) {
|
||||
await window.recipeManager.loadRecipes(true);
|
||||
} else if (this.currentPage === 'loras') {
|
||||
await loadMoreLoras(true, true);
|
||||
} else if (this.currentPage === 'checkpoints' && window.checkpointManager) {
|
||||
await window.checkpointManager.loadCheckpoints(true);
|
||||
}
|
||||
|
||||
showToast(`Filters cleared`, 'info');
|
||||
}
|
||||
|
||||
loadFiltersFromStorage() {
|
||||
const savedFilters = localStorage.getItem('loraFilters');
|
||||
const storageKey = `${this.currentPage}_filters`;
|
||||
const savedFilters = localStorage.getItem(storageKey);
|
||||
|
||||
if (savedFilters) {
|
||||
try {
|
||||
const parsedFilters = JSON.parse(savedFilters);
|
||||
@@ -283,6 +374,10 @@ export class FilterManager {
|
||||
tags: parsedFilters.tags || []
|
||||
};
|
||||
|
||||
// Update state with loaded filters
|
||||
const pageState = getCurrentPageState();
|
||||
pageState.filters = { ...this.filters };
|
||||
|
||||
this.updateTagSelections();
|
||||
this.updateActiveFiltersCount();
|
||||
|
||||
@@ -290,7 +385,7 @@ export class FilterManager {
|
||||
this.filterButton.classList.add('active');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading filters from storage:', error);
|
||||
console.error(`Error loading ${this.currentPage} filters from storage:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
/**
|
||||
* LoraSearchManager - Specialized search manager for the LoRAs page
|
||||
* Extends the base SearchManager with LoRA-specific functionality
|
||||
*/
|
||||
import { SearchManager } from './SearchManager.js';
|
||||
import { appendLoraCards } from '../api/loraApi.js';
|
||||
import { resetAndReload } from '../api/loraApi.js';
|
||||
import { state, getCurrentPageState } from '../state/index.js';
|
||||
import { showToast } from '../utils/uiHelpers.js';
|
||||
|
||||
export class LoraSearchManager extends SearchManager {
|
||||
constructor(options = {}) {
|
||||
super({
|
||||
page: 'loras',
|
||||
...options
|
||||
});
|
||||
|
||||
this.currentSearchTerm = '';
|
||||
|
||||
// Store this instance in the state
|
||||
if (state) {
|
||||
const pageState = getCurrentPageState();
|
||||
pageState.searchManager = this;
|
||||
}
|
||||
}
|
||||
|
||||
async performSearch() {
|
||||
const searchTerm = this.searchInput.value.trim().toLowerCase();
|
||||
const pageState = getCurrentPageState();
|
||||
|
||||
// Log the search attempt for debugging
|
||||
console.log('LoraSearchManager performSearch called with:', searchTerm);
|
||||
|
||||
if (searchTerm === this.currentSearchTerm && !this.isSearching) {
|
||||
return; // Avoid duplicate searches
|
||||
}
|
||||
|
||||
this.currentSearchTerm = searchTerm;
|
||||
|
||||
const grid = document.getElementById('loraGrid');
|
||||
if (!grid) {
|
||||
console.error('Error: Could not find loraGrid element');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!searchTerm) {
|
||||
if (pageState) {
|
||||
pageState.currentPage = 1;
|
||||
}
|
||||
await resetAndReload();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.isSearching = true;
|
||||
if (state && state.loadingManager) {
|
||||
state.loadingManager.showSimpleLoading('Searching...');
|
||||
}
|
||||
|
||||
// Store current scroll position
|
||||
const scrollPosition = window.pageYOffset || document.documentElement.scrollTop;
|
||||
|
||||
if (pageState) {
|
||||
pageState.currentPage = 1;
|
||||
pageState.hasMore = true;
|
||||
}
|
||||
|
||||
const url = new URL('/api/loras', window.location.origin);
|
||||
url.searchParams.set('page', '1');
|
||||
url.searchParams.set('page_size', '20');
|
||||
url.searchParams.set('sort_by', pageState ? pageState.sortBy : 'name');
|
||||
url.searchParams.set('search', searchTerm);
|
||||
url.searchParams.set('fuzzy', 'true');
|
||||
|
||||
// Add search options
|
||||
const searchOptions = this.getActiveSearchOptions();
|
||||
console.log('Active search options:', searchOptions);
|
||||
|
||||
// Make sure we're sending boolean values as strings
|
||||
url.searchParams.set('search_filename', searchOptions.filename ? 'true' : 'false');
|
||||
url.searchParams.set('search_modelname', searchOptions.modelname ? 'true' : 'false');
|
||||
url.searchParams.set('search_tags', searchOptions.tags ? 'true' : 'false');
|
||||
|
||||
// Always send folder parameter if there is an active folder
|
||||
if (pageState && pageState.activeFolder) {
|
||||
url.searchParams.set('folder', pageState.activeFolder);
|
||||
// Add recursive parameter when recursive search is enabled
|
||||
const recursive = this.recursiveSearchToggle ? this.recursiveSearchToggle.checked : false;
|
||||
url.searchParams.set('recursive', recursive.toString());
|
||||
}
|
||||
|
||||
console.log('Search URL:', url.toString());
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Search failed with status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Search results:', data);
|
||||
|
||||
if (searchTerm === this.currentSearchTerm) {
|
||||
grid.innerHTML = '';
|
||||
|
||||
if (data.items.length === 0) {
|
||||
grid.innerHTML = '<div class="no-results">No matching loras found</div>';
|
||||
if (pageState) {
|
||||
pageState.hasMore = false;
|
||||
}
|
||||
} else {
|
||||
appendLoraCards(data.items);
|
||||
if (pageState) {
|
||||
pageState.hasMore = pageState.currentPage < data.total_pages;
|
||||
pageState.currentPage++;
|
||||
}
|
||||
}
|
||||
|
||||
// Restore scroll position after content is loaded
|
||||
setTimeout(() => {
|
||||
window.scrollTo({
|
||||
top: scrollPosition,
|
||||
behavior: 'instant' // Use 'instant' to prevent animation
|
||||
});
|
||||
}, 10);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Search error:', error);
|
||||
showToast('Search failed', 'error');
|
||||
} finally {
|
||||
this.isSearching = false;
|
||||
if (state && state.loadingManager) {
|
||||
state.loadingManager.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,356 +0,0 @@
|
||||
import { showToast, updatePanelPositions } from '../utils/uiHelpers.js';
|
||||
|
||||
export class RecipeFilterManager {
|
||||
constructor() {
|
||||
this.filters = {
|
||||
baseModel: [],
|
||||
tags: []
|
||||
};
|
||||
|
||||
this.filterPanel = document.getElementById('filterPanel');
|
||||
this.filterButton = document.getElementById('filterButton');
|
||||
this.activeFiltersCount = document.getElementById('activeFiltersCount');
|
||||
this.tagsLoaded = false;
|
||||
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
initialize() {
|
||||
// Create base model filter tags if they exist
|
||||
if (document.getElementById('baseModelTags')) {
|
||||
this.createBaseModelTags();
|
||||
}
|
||||
|
||||
// Close filter panel when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!this.filterPanel.contains(e.target) &&
|
||||
e.target !== this.filterButton &&
|
||||
!this.filterButton.contains(e.target) &&
|
||||
!this.filterPanel.classList.contains('hidden')) {
|
||||
this.closeFilterPanel();
|
||||
}
|
||||
});
|
||||
|
||||
// Add click handler for filter button
|
||||
if (this.filterButton) {
|
||||
this.filterButton.addEventListener('click', () => {
|
||||
this.toggleFilterPanel();
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize active filters from localStorage if available
|
||||
this.loadFiltersFromStorage();
|
||||
}
|
||||
|
||||
async loadTopTags() {
|
||||
try {
|
||||
// Show loading state
|
||||
const tagsContainer = document.getElementById('modelTagsFilter');
|
||||
if (tagsContainer) {
|
||||
tagsContainer.innerHTML = '<div class="tags-loading">Loading tags...</div>';
|
||||
}
|
||||
|
||||
const response = await fetch('/api/recipes/top-tags?limit=20');
|
||||
if (!response.ok) throw new Error('Failed to fetch recipe tags');
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success && data.tags) {
|
||||
this.createTagFilterElements(data.tags);
|
||||
|
||||
// After creating tag elements, mark any previously selected ones
|
||||
this.updateTagSelections();
|
||||
} else {
|
||||
throw new Error('Invalid response format');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading top recipe tags:', error);
|
||||
const tagsContainer = document.getElementById('modelTagsFilter');
|
||||
if (tagsContainer) {
|
||||
tagsContainer.innerHTML = '<div class="tags-error">Failed to load tags</div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createTagFilterElements(tags) {
|
||||
const tagsContainer = document.getElementById('modelTagsFilter');
|
||||
if (!tagsContainer) return;
|
||||
|
||||
tagsContainer.innerHTML = '';
|
||||
|
||||
if (!tags.length) {
|
||||
tagsContainer.innerHTML = '<div class="no-tags">No recipe tags available</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
tags.forEach(tag => {
|
||||
const tagEl = document.createElement('div');
|
||||
tagEl.className = 'filter-tag tag-filter';
|
||||
const tagName = tag.tag;
|
||||
tagEl.dataset.tag = tagName;
|
||||
tagEl.innerHTML = `${tagName} <span class="tag-count">${tag.count}</span>`;
|
||||
|
||||
// Add click handler to toggle selection and automatically apply
|
||||
tagEl.addEventListener('click', async () => {
|
||||
tagEl.classList.toggle('active');
|
||||
|
||||
if (tagEl.classList.contains('active')) {
|
||||
if (!this.filters.tags.includes(tagName)) {
|
||||
this.filters.tags.push(tagName);
|
||||
}
|
||||
} else {
|
||||
this.filters.tags = this.filters.tags.filter(t => t !== tagName);
|
||||
}
|
||||
|
||||
this.updateActiveFiltersCount();
|
||||
|
||||
// Auto-apply filter when tag is clicked
|
||||
await this.applyFilters(false);
|
||||
});
|
||||
|
||||
tagsContainer.appendChild(tagEl);
|
||||
});
|
||||
}
|
||||
|
||||
createBaseModelTags() {
|
||||
const baseModelTagsContainer = document.getElementById('baseModelTags');
|
||||
if (!baseModelTagsContainer) return;
|
||||
|
||||
// Fetch base models used in recipes
|
||||
fetch('/api/recipes/base-models')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success && data.base_models) {
|
||||
baseModelTagsContainer.innerHTML = '';
|
||||
|
||||
data.base_models.forEach(model => {
|
||||
const tag = document.createElement('div');
|
||||
tag.className = `filter-tag base-model-tag`;
|
||||
tag.dataset.baseModel = model.name;
|
||||
tag.innerHTML = `${model.name} <span class="tag-count">${model.count}</span>`;
|
||||
|
||||
// Add click handler to toggle selection and automatically apply
|
||||
tag.addEventListener('click', async () => {
|
||||
tag.classList.toggle('active');
|
||||
|
||||
if (tag.classList.contains('active')) {
|
||||
if (!this.filters.baseModel.includes(model.name)) {
|
||||
this.filters.baseModel.push(model.name);
|
||||
}
|
||||
} else {
|
||||
this.filters.baseModel = this.filters.baseModel.filter(m => m !== model.name);
|
||||
}
|
||||
|
||||
this.updateActiveFiltersCount();
|
||||
|
||||
// Auto-apply filter when tag is clicked
|
||||
await this.applyFilters(false);
|
||||
});
|
||||
|
||||
baseModelTagsContainer.appendChild(tag);
|
||||
});
|
||||
|
||||
// Update selections based on stored filters
|
||||
this.updateTagSelections();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching base models:', error);
|
||||
baseModelTagsContainer.innerHTML = '<div class="tags-error">Failed to load base models</div>';
|
||||
});
|
||||
}
|
||||
|
||||
toggleFilterPanel() {
|
||||
if (this.filterPanel) {
|
||||
const isHidden = this.filterPanel.classList.contains('hidden');
|
||||
|
||||
if (isHidden) {
|
||||
// Update panel positions before showing
|
||||
updatePanelPositions();
|
||||
|
||||
this.filterPanel.classList.remove('hidden');
|
||||
this.filterButton.classList.add('active');
|
||||
|
||||
// Load tags if they haven't been loaded yet
|
||||
if (!this.tagsLoaded) {
|
||||
this.loadTopTags();
|
||||
this.tagsLoaded = true;
|
||||
}
|
||||
} else {
|
||||
this.closeFilterPanel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
closeFilterPanel() {
|
||||
if (this.filterPanel) {
|
||||
this.filterPanel.classList.add('hidden');
|
||||
}
|
||||
if (this.filterButton) {
|
||||
this.filterButton.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
updateTagSelections() {
|
||||
// Update base model tags
|
||||
const baseModelTags = document.querySelectorAll('.base-model-tag');
|
||||
baseModelTags.forEach(tag => {
|
||||
const baseModel = tag.dataset.baseModel;
|
||||
if (this.filters.baseModel.includes(baseModel)) {
|
||||
tag.classList.add('active');
|
||||
} else {
|
||||
tag.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
// Update model tags
|
||||
const modelTags = document.querySelectorAll('.tag-filter');
|
||||
modelTags.forEach(tag => {
|
||||
const tagName = tag.dataset.tag;
|
||||
if (this.filters.tags.includes(tagName)) {
|
||||
tag.classList.add('active');
|
||||
} else {
|
||||
tag.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateActiveFiltersCount() {
|
||||
const totalActiveFilters = this.filters.baseModel.length + this.filters.tags.length;
|
||||
|
||||
if (totalActiveFilters > 0) {
|
||||
this.activeFiltersCount.textContent = totalActiveFilters;
|
||||
this.activeFiltersCount.style.display = 'inline-flex';
|
||||
} else {
|
||||
this.activeFiltersCount.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
async applyFilters(showToastNotification = true) {
|
||||
// Save filters to localStorage
|
||||
localStorage.setItem('recipeFilters', JSON.stringify(this.filters));
|
||||
|
||||
// Reload recipes with filters applied
|
||||
if (window.recipeManager && typeof window.recipeManager.loadRecipes === 'function') {
|
||||
try {
|
||||
// Show loading state if available
|
||||
if (window.recipeManager.showLoading) {
|
||||
window.recipeManager.showLoading();
|
||||
}
|
||||
|
||||
// Apply the filters
|
||||
await window.recipeManager.loadRecipes(this.filters);
|
||||
|
||||
// Hide loading state if available
|
||||
if (window.recipeManager.hideLoading) {
|
||||
window.recipeManager.hideLoading();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error applying filters:', error);
|
||||
// Fallback to page reload
|
||||
this._applyFiltersByPageReload();
|
||||
}
|
||||
} else {
|
||||
// Fallback to page reload with filter parameters
|
||||
this._applyFiltersByPageReload();
|
||||
}
|
||||
|
||||
// Update filter button to show active state
|
||||
if (this.hasActiveFilters()) {
|
||||
this.filterButton.classList.add('active');
|
||||
if (showToastNotification) {
|
||||
const baseModelCount = this.filters.baseModel.length;
|
||||
const tagsCount = this.filters.tags.length;
|
||||
|
||||
let message = '';
|
||||
if (baseModelCount > 0 && tagsCount > 0) {
|
||||
message = `Filtering by ${baseModelCount} base model${baseModelCount > 1 ? 's' : ''} and ${tagsCount} tag${tagsCount > 1 ? 's' : ''}`;
|
||||
} else if (baseModelCount > 0) {
|
||||
message = `Filtering by ${baseModelCount} base model${baseModelCount > 1 ? 's' : ''}`;
|
||||
} else if (tagsCount > 0) {
|
||||
message = `Filtering by ${tagsCount} tag${tagsCount > 1 ? 's' : ''}`;
|
||||
}
|
||||
|
||||
showToast(message, 'success');
|
||||
}
|
||||
} else {
|
||||
this.filterButton.classList.remove('active');
|
||||
if (showToastNotification) {
|
||||
showToast('Filters cleared', 'info');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add a helper method for page reload fallback
|
||||
_applyFiltersByPageReload() {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (this.filters.baseModel.length > 0) {
|
||||
params.append('base_models', this.filters.baseModel.join(','));
|
||||
}
|
||||
|
||||
if (this.filters.tags.length > 0) {
|
||||
params.append('tags', this.filters.tags.join(','));
|
||||
}
|
||||
|
||||
if (params.toString()) {
|
||||
window.location.href = `/loras/recipes?${params.toString()}`;
|
||||
} else {
|
||||
window.location.href = '/loras/recipes';
|
||||
}
|
||||
}
|
||||
|
||||
async clearFilters() {
|
||||
// Clear all filters
|
||||
this.filters = {
|
||||
baseModel: [],
|
||||
tags: []
|
||||
};
|
||||
|
||||
// Update UI
|
||||
this.updateTagSelections();
|
||||
this.updateActiveFiltersCount();
|
||||
|
||||
// Remove from localStorage
|
||||
localStorage.removeItem('recipeFilters');
|
||||
|
||||
// Update UI and reload data
|
||||
this.filterButton.classList.remove('active');
|
||||
|
||||
// Reload recipes without filters
|
||||
if (window.recipeManager && typeof window.recipeManager.loadRecipes === 'function') {
|
||||
await window.recipeManager.loadRecipes();
|
||||
} else {
|
||||
window.location.href = '/loras/recipes';
|
||||
}
|
||||
|
||||
showToast('Recipe filters cleared', 'info');
|
||||
}
|
||||
|
||||
loadFiltersFromStorage() {
|
||||
const savedFilters = localStorage.getItem('recipeFilters');
|
||||
if (savedFilters) {
|
||||
try {
|
||||
const parsedFilters = JSON.parse(savedFilters);
|
||||
|
||||
// Ensure backward compatibility with older filter format
|
||||
this.filters = {
|
||||
baseModel: parsedFilters.baseModel || [],
|
||||
tags: parsedFilters.tags || []
|
||||
};
|
||||
|
||||
this.updateTagSelections();
|
||||
this.updateActiveFiltersCount();
|
||||
|
||||
if (this.hasActiveFilters()) {
|
||||
this.filterButton.classList.add('active');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading recipe filters from storage:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hasActiveFilters() {
|
||||
return this.filters.baseModel.length > 0 || this.filters.tags.length > 0;
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
/**
|
||||
* RecipeSearchManager - Specialized search manager for the Recipes page
|
||||
* Extends the base SearchManager with recipe-specific functionality
|
||||
*/
|
||||
import { SearchManager } from './SearchManager.js';
|
||||
import { state, getCurrentPageState } from '../state/index.js';
|
||||
import { showToast } from '../utils/uiHelpers.js';
|
||||
|
||||
export class RecipeSearchManager extends SearchManager {
|
||||
constructor(options = {}) {
|
||||
super({
|
||||
page: 'recipes',
|
||||
...options
|
||||
});
|
||||
|
||||
this.currentSearchTerm = '';
|
||||
|
||||
// Store this instance in the state
|
||||
if (state) {
|
||||
state.pages.recipes.searchManager = this;
|
||||
}
|
||||
}
|
||||
|
||||
async performSearch() {
|
||||
const searchTerm = this.searchInput.value.trim().toLowerCase();
|
||||
|
||||
if (searchTerm === this.currentSearchTerm && !this.isSearching) {
|
||||
return; // Avoid duplicate searches
|
||||
}
|
||||
|
||||
this.currentSearchTerm = searchTerm;
|
||||
|
||||
const grid = document.getElementById('recipeGrid');
|
||||
|
||||
if (!searchTerm) {
|
||||
window.recipeManager.loadRecipes();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.isSearching = true;
|
||||
if (state && state.loadingManager) {
|
||||
state.loadingManager.showSimpleLoading('Searching recipes...');
|
||||
}
|
||||
|
||||
// Store current scroll position
|
||||
const scrollPosition = window.pageYOffset || document.documentElement.scrollTop;
|
||||
|
||||
if (state) {
|
||||
state.pages.recipes.currentPage = 1;
|
||||
state.pages.recipes.hasMore = true;
|
||||
}
|
||||
|
||||
const url = new URL('/api/recipes', window.location.origin);
|
||||
url.searchParams.set('page', '1');
|
||||
url.searchParams.set('page_size', '20');
|
||||
url.searchParams.set('sort_by', state ? state.pages.recipes.sortBy : 'name');
|
||||
url.searchParams.set('search', searchTerm);
|
||||
url.searchParams.set('fuzzy', 'true');
|
||||
|
||||
// Add search options
|
||||
const recipeState = getCurrentPageState();
|
||||
const searchOptions = recipeState.searchOptions;
|
||||
url.searchParams.set('search_title', searchOptions.title.toString());
|
||||
url.searchParams.set('search_tags', searchOptions.tags.toString());
|
||||
url.searchParams.set('search_lora_name', searchOptions.loraName.toString());
|
||||
url.searchParams.set('search_lora_model', searchOptions.loraModel.toString());
|
||||
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Search failed');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (searchTerm === this.currentSearchTerm && grid) {
|
||||
grid.innerHTML = '';
|
||||
|
||||
if (data.items.length === 0) {
|
||||
grid.innerHTML = '<div class="no-results">No matching recipes found</div>';
|
||||
if (state) {
|
||||
state.pages.recipes.hasMore = false;
|
||||
}
|
||||
} else {
|
||||
this.appendRecipeCards(data.items);
|
||||
if (state) {
|
||||
state.pages.recipes.hasMore = state.pages.recipes.currentPage < data.total_pages;
|
||||
state.pages.recipes.currentPage++;
|
||||
}
|
||||
}
|
||||
|
||||
// Restore scroll position after content is loaded
|
||||
setTimeout(() => {
|
||||
window.scrollTo({
|
||||
top: scrollPosition,
|
||||
behavior: 'instant' // Use 'instant' to prevent animation
|
||||
});
|
||||
}, 10);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Recipe search error:', error);
|
||||
showToast('Recipe search failed', 'error');
|
||||
} finally {
|
||||
this.isSearching = false;
|
||||
if (state && state.loadingManager) {
|
||||
state.loadingManager.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
appendRecipeCards(recipes) {
|
||||
const grid = document.getElementById('recipeGrid');
|
||||
if (!grid) return;
|
||||
|
||||
// Create data object in the format expected by the RecipeManager
|
||||
const data = { items: recipes, has_more: false };
|
||||
window.recipeManager.updateRecipesGrid(data, false);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { updatePanelPositions } from "../utils/uiHelpers.js";
|
||||
import { getCurrentPageState } from "../state/index.js";
|
||||
/**
|
||||
* SearchManager - Handles search functionality across different pages
|
||||
* Each page can extend or customize this base functionality
|
||||
@@ -272,24 +273,52 @@ export class SearchManager {
|
||||
const options = this.getActiveSearchOptions();
|
||||
const recursive = this.recursiveSearchToggle ? this.recursiveSearchToggle.checked : false;
|
||||
|
||||
// This is a base implementation - each page should override this method
|
||||
console.log('Performing search:', {
|
||||
query,
|
||||
options,
|
||||
recursive,
|
||||
page: this.currentPage
|
||||
});
|
||||
// Update the state with search parameters
|
||||
const pageState = getCurrentPageState();
|
||||
|
||||
// Dispatch a custom event that page-specific code can listen for
|
||||
const searchEvent = new CustomEvent('app:search', {
|
||||
detail: {
|
||||
query,
|
||||
options,
|
||||
recursive,
|
||||
page: this.currentPage
|
||||
// Set search query in filters
|
||||
if (pageState && pageState.filters) {
|
||||
pageState.filters.search = query;
|
||||
}
|
||||
|
||||
// Update search options based on page type
|
||||
if (pageState && pageState.searchOptions) {
|
||||
if (this.currentPage === 'recipes') {
|
||||
pageState.searchOptions = {
|
||||
title: options.title || false,
|
||||
tags: options.tags || false,
|
||||
loraName: options.loraName || false,
|
||||
loraModel: options.loraModel || false
|
||||
};
|
||||
} else if (this.currentPage === 'loras') {
|
||||
pageState.searchOptions = {
|
||||
filename: options.filename || false,
|
||||
modelname: options.modelname || false,
|
||||
tags: options.tags || false,
|
||||
recursive: recursive
|
||||
};
|
||||
} else if (this.currentPage === 'checkpoints') {
|
||||
pageState.searchOptions = {
|
||||
filename: options.filename || false,
|
||||
modelname: options.modelname || false,
|
||||
recursive: recursive
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.dispatchEvent(searchEvent);
|
||||
// Call the appropriate manager's load method based on page type
|
||||
if (this.currentPage === 'recipes' && window.recipeManager) {
|
||||
console.log("load recipes")
|
||||
window.recipeManager.loadRecipes(true); // true to reset pagination
|
||||
} else if (this.currentPage === 'loras' && window.loadMoreLoras) {
|
||||
// Reset loras page and reload
|
||||
if (pageState) {
|
||||
pageState.currentPage = 1;
|
||||
pageState.hasMore = true;
|
||||
}
|
||||
window.loadMoreLoras(true); // true to reset pagination
|
||||
} else if (this.currentPage === 'checkpoints' && window.checkpointManager) {
|
||||
window.checkpointManager.loadCheckpoints(true); // true to reset pagination
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user