From addf92d9665e396f33344421846c9a657177bb41 Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Thu, 20 Mar 2025 14:54:13 +0800 Subject: [PATCH] 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. --- py/routes/api_routes.py | 29 +- py/routes/recipe_routes.py | 8 +- py/routes/update_routes.py | 6 +- static/js/api/loraApi.js | 45 +-- static/js/components/Header.js | 44 +-- static/js/managers/FilterManager.js | 195 +++++++++--- static/js/managers/LoraSearchManager.js | 136 --------- static/js/managers/RecipeFilterManager.js | 356 ---------------------- static/js/managers/RecipeSearchManager.js | 120 -------- static/js/managers/SearchManager.js | 61 +++- static/js/recipes.js | 30 +- static/js/utils/infiniteScroll.js | 16 +- 12 files changed, 264 insertions(+), 782 deletions(-) delete mode 100644 static/js/managers/LoraSearchManager.js delete mode 100644 static/js/managers/RecipeFilterManager.js delete mode 100644 static/js/managers/RecipeSearchManager.js diff --git a/py/routes/api_routes.py b/py/routes/api_routes.py index 3bf20778..ceb685b3 100644 --- a/py/routes/api_routes.py +++ b/py/routes/api_routes.py @@ -50,7 +50,6 @@ class ApiRoutes: app.router.add_get('/api/lora-preview-url', routes.get_lora_preview_url) # Add new route app.router.add_post('/api/move_models_bulk', routes.move_models_bulk) app.router.add_get('/api/top-tags', routes.get_top_tags) # Add new route for top tags - app.router.add_get('/api/recipes', cls.handle_get_recipes) # Add update check routes UpdateRoutes.setup_routes(app) @@ -842,30 +841,4 @@ class ApiRoutes: return web.json_response({ 'success': False, 'error': 'Internal server error' - }, status=500) - - @staticmethod - async def handle_get_recipes(request): - """API endpoint for getting paginated recipes""" - try: - # Get query parameters with defaults - page = int(request.query.get('page', '1')) - page_size = int(request.query.get('page_size', '20')) - sort_by = request.query.get('sort_by', 'date') - search = request.query.get('search', None) - - # Get scanner instance - scanner = RecipeScanner(LoraScanner()) - - # Get paginated data - result = await scanner.get_paginated_data( - page=page, - page_size=page_size, - sort_by=sort_by, - search=search - ) - - return web.json_response(result) - except Exception as e: - logger.error(f"Error retrieving recipes: {e}", exc_info=True) - return web.json_response({"error": str(e)}, status=500) + }, status=500) \ No newline at end of file diff --git a/py/routes/recipe_routes.py b/py/routes/recipe_routes.py index 9678105a..63be56e5 100644 --- a/py/routes/recipe_routes.py +++ b/py/routes/recipe_routes.py @@ -1,11 +1,9 @@ import os import logging -import sys from aiohttp import web from typing import Dict import tempfile import json -import aiohttp import asyncio from ..utils.exif_utils import ExifUtils from ..utils.recipe_parsers import RecipeParserFactory @@ -70,6 +68,7 @@ class RecipeRoutes: async def get_recipes(self, request: web.Request) -> web.Response: """API endpoint for getting paginated recipes""" try: + logger.info(f"get_recipes, Request: {request}") # Get query parameters with defaults page = int(request.query.get('page', '1')) page_size = int(request.query.get('page_size', '20')) @@ -100,7 +99,8 @@ class RecipeRoutes: 'lora_name': search_lora_name, 'lora_model': search_lora_model } - + + logger.info(f"get_recipes, Filters: {filters}, Search Options: {search_options}") # Get paginated data result = await self.recipe_scanner.get_paginated_data( page=page, @@ -136,7 +136,7 @@ class RecipeRoutes: """Get detailed information about a specific recipe""" try: recipe_id = request.match_info['recipe_id'] - + # Get all recipes from cache cache = await self.recipe_scanner.get_cached_data() diff --git a/py/routes/update_routes.py b/py/routes/update_routes.py index 41037aae..5b488d55 100644 --- a/py/routes/update_routes.py +++ b/py/routes/update_routes.py @@ -24,11 +24,9 @@ class UpdateRoutes: try: # Read local version from pyproject.toml local_version = UpdateRoutes._get_local_version() - logger.info(f"Local version: {local_version}") - + # Fetch remote version from GitHub remote_version, changelog = await UpdateRoutes._get_remote_version() - logger.info(f"Remote version: {remote_version}") # Compare versions update_available = UpdateRoutes._compare_versions( @@ -36,8 +34,6 @@ class UpdateRoutes: remote_version.replace('v', '') ) - logger.info(f"Update available: {update_available}") - return web.json_response({ 'success': True, 'current_version': local_version, diff --git a/static/js/api/loraApi.js b/static/js/api/loraApi.js index 781739ea..3c63c74a 100644 --- a/static/js/api/loraApi.js +++ b/static/js/api/loraApi.js @@ -5,13 +5,21 @@ import { initializeInfiniteScroll } from '../utils/infiniteScroll.js'; import { showDeleteModal } from '../utils/modalUtils.js'; import { toggleFolder } from '../utils/uiHelpers.js'; -export async function loadMoreLoras(boolUpdateFolders = false) { +export async function loadMoreLoras(resetPage = false, updateFolders = false) { const pageState = getCurrentPageState(); - if (pageState.isLoading || !pageState.hasMore) return; + if (pageState.isLoading || (!pageState.hasMore && !resetPage)) return; pageState.isLoading = true; try { + // Reset to first page if requested + if (resetPage) { + pageState.currentPage = 1; + // Clear grid if resetting + const grid = document.getElementById('loraGrid'); + if (grid) grid.innerHTML = ''; + } + const params = new URLSearchParams({ page: pageState.currentPage, page_size: 20, @@ -19,7 +27,7 @@ export async function loadMoreLoras(boolUpdateFolders = false) { }); // Use pageState instead of state - const isRecursiveSearch = pageState.searchManager?.isRecursiveSearch ?? false; + const isRecursiveSearch = pageState.searchOptions?.recursive ?? false; if (pageState.activeFolder !== null) { params.append('folder', pageState.activeFolder); @@ -27,10 +35,16 @@ export async function loadMoreLoras(boolUpdateFolders = false) { } // Add search parameters if there's a search term - const searchInput = document.getElementById('searchInput'); - if (searchInput && searchInput.value.trim()) { - params.append('search', searchInput.value.trim()); + if (pageState.filters?.search) { + params.append('search', pageState.filters.search); params.append('fuzzy', 'true'); + + // Add search option parameters if available + if (pageState.searchOptions) { + params.append('search_filename', pageState.searchOptions.filename.toString()); + params.append('search_modelname', pageState.searchOptions.modelname.toString()); + params.append('search_tags', (pageState.searchOptions.tags || false).toString()); + } } // Add filter parameters if active @@ -72,7 +86,7 @@ export async function loadMoreLoras(boolUpdateFolders = false) { pageState.hasMore = false; } - if (boolUpdateFolders && data.folders) { + if (updateFolders && data.folders) { updateFolderTags(data.folders); } @@ -271,24 +285,15 @@ export function appendLoraCards(loras) { }); } -export async function resetAndReload(boolUpdateFolders = false) { +export async function resetAndReload(updateFolders = false) { const pageState = getCurrentPageState(); console.log('Resetting with state:', { ...pageState }); - pageState.currentPage = 1; - pageState.hasMore = true; - pageState.isLoading = false; - - const grid = document.getElementById('loraGrid'); - grid.innerHTML = ''; - - const sentinel = document.createElement('div'); - sentinel.id = 'scroll-sentinel'; - grid.appendChild(sentinel); - + // Initialize infinite scroll - will reset the observer initializeInfiniteScroll(); - await loadMoreLoras(boolUpdateFolders); + // Load more loras with reset flag + await loadMoreLoras(true, updateFolders); } export async function refreshLoras() { diff --git a/static/js/components/Header.js b/static/js/components/Header.js index 710abb24..5332c706 100644 --- a/static/js/components/Header.js +++ b/static/js/components/Header.js @@ -1,5 +1,7 @@ import { updateService } from '../managers/UpdateService.js'; import { toggleTheme } from '../utils/uiHelpers.js'; +import { SearchManager } from '../managers/SearchManager.js'; +import { FilterManager } from '../managers/FilterManager.js'; /** * Header.js - Manages the application header behavior across different pages @@ -27,40 +29,14 @@ export class HeaderManager { } initializeManagers() { - // Import and initialize appropriate search manager based on page - if (this.currentPage === 'loras') { - import('../managers/LoraSearchManager.js').then(module => { - const { LoraSearchManager } = module; - this.searchManager = new LoraSearchManager(); - window.searchManager = this.searchManager; - }); - - import('../managers/FilterManager.js').then(module => { - const { FilterManager } = module; - this.filterManager = new FilterManager(); - window.filterManager = this.filterManager; - }); - } else if (this.currentPage === 'recipes') { - import('../managers/RecipeSearchManager.js').then(module => { - const { RecipeSearchManager } = module; - this.searchManager = new RecipeSearchManager(); - window.searchManager = this.searchManager; - }); - - import('../managers/RecipeFilterManager.js').then(module => { - const { RecipeFilterManager } = module; - this.filterManager = new RecipeFilterManager(); - window.filterManager = this.filterManager; - }); - } else if (this.currentPage === 'checkpoints') { - import('../managers/CheckpointSearchManager.js').then(module => { - const { CheckpointSearchManager } = module; - this.searchManager = new CheckpointSearchManager(); - window.searchManager = this.searchManager; - }); - - // Note: Checkpoints page might get its own filter manager in the future - // For now, we can use a basic filter manager or none at all + // Initialize SearchManager for all page types + this.searchManager = new SearchManager({ page: this.currentPage }); + window.searchManager = this.searchManager; + + // Initialize FilterManager for all page types that have filters + if (document.getElementById('filterButton')) { + this.filterManager = new FilterManager({ page: this.currentPage }); + window.filterManager = this.filterManager; } } diff --git a/static/js/managers/FilterManager.js b/static/js/managers/FilterManager.js index 5be55328..11e49e8b 100644 --- a/static/js/managers/FilterManager.js +++ b/static/js/managers/FilterManager.js @@ -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 = '
Loading tags...
'; + if (!tagsContainer) return; + + tagsContainer.innerHTML = '
Loading tags...
'; + + // 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 = '
No tags available
'; + tagsContainer.innerHTML = `
No ${this.currentPage === 'recipes' ? 'recipe ' : ''}tags available
`; 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} ${tag.count}`; @@ -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} ${model.count}`; + + // 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 = '
Failed to load base models
'; + }); + } } 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); } } } diff --git a/static/js/managers/LoraSearchManager.js b/static/js/managers/LoraSearchManager.js deleted file mode 100644 index bae212b3..00000000 --- a/static/js/managers/LoraSearchManager.js +++ /dev/null @@ -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 = '
No matching loras found
'; - 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(); - } - } - } -} \ No newline at end of file diff --git a/static/js/managers/RecipeFilterManager.js b/static/js/managers/RecipeFilterManager.js deleted file mode 100644 index 5cb5fc73..00000000 --- a/static/js/managers/RecipeFilterManager.js +++ /dev/null @@ -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 = '
Loading tags...
'; - } - - 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 = '
Failed to load tags
'; - } - } - } - - createTagFilterElements(tags) { - const tagsContainer = document.getElementById('modelTagsFilter'); - if (!tagsContainer) return; - - tagsContainer.innerHTML = ''; - - if (!tags.length) { - tagsContainer.innerHTML = '
No recipe tags available
'; - 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} ${tag.count}`; - - // 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} ${model.count}`; - - // 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 = '
Failed to load base models
'; - }); - } - - 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; - } -} \ No newline at end of file diff --git a/static/js/managers/RecipeSearchManager.js b/static/js/managers/RecipeSearchManager.js deleted file mode 100644 index 93dd6f2b..00000000 --- a/static/js/managers/RecipeSearchManager.js +++ /dev/null @@ -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 = '
No matching recipes found
'; - 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); - } -} \ No newline at end of file diff --git a/static/js/managers/SearchManager.js b/static/js/managers/SearchManager.js index 0dc552ba..d4d48f28 100644 --- a/static/js/managers/SearchManager.js +++ b/static/js/managers/SearchManager.js @@ -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 + } } } \ No newline at end of file diff --git a/static/js/recipes.js b/static/js/recipes.js index 3598d8fc..af7fcae1 100644 --- a/static/js/recipes.js +++ b/static/js/recipes.js @@ -58,10 +58,17 @@ class RecipeManager { window.recipeManager = this; window.importRecipes = () => this.importRecipes(); window.importManager = this.importManager; - window.loadMoreRecipes = () => this.loadMoreRecipes(); - // Add appendRecipeCards function for the search manager to use + // Deprecated - kept for backwards compatibility + window.loadMoreRecipes = () => { + console.warn('loadMoreRecipes is deprecated, use infiniteScroll instead'); + this.pageState.currentPage++; + this.loadRecipes(false); + }; + + // Add appendRecipeCards function for compatibility window.appendRecipeCards = (recipes) => { + console.warn('appendRecipeCards is deprecated, use recipeManager.updateRecipesGrid instead'); const data = { items: recipes, has_more: false }; this.updateRecipesGrid(data, false); }; @@ -102,6 +109,15 @@ class RecipeManager { // Add search filter if present if (this.pageState.filters.search) { params.append('search', this.pageState.filters.search); + + // Add search option parameters + if (this.pageState.searchOptions) { + params.append('search_title', this.pageState.searchOptions.title.toString()); + params.append('search_tags', this.pageState.searchOptions.tags.toString()); + params.append('search_lora_name', this.pageState.searchOptions.loraName.toString()); + params.append('search_lora_model', this.pageState.searchOptions.loraModel.toString()); + params.append('fuzzy', 'true'); + } } // Add base model filters @@ -113,6 +129,8 @@ class RecipeManager { if (this.pageState.filters.tags && this.pageState.filters.tags.length) { params.append('tags', this.pageState.filters.tags.join(',')); } + + console.log('Loading recipes with params:', params.toString()); // Fetch recipes const response = await fetch(`/api/recipes?${params.toString()}`); @@ -139,14 +157,6 @@ class RecipeManager { } } - // Load more recipes for infinite scroll - async loadMoreRecipes() { - if (this.pageState.isLoading || !this.pageState.hasMore) return; - - this.pageState.currentPage++; - await this.loadRecipes(false); - } - updateRecipesGrid(data, resetGrid = true) { const grid = document.getElementById('recipeGrid'); if (!grid) return; diff --git a/static/js/utils/infiniteScroll.js b/static/js/utils/infiniteScroll.js index 5e95e5a8..0c54216b 100644 --- a/static/js/utils/infiniteScroll.js +++ b/static/js/utils/infiniteScroll.js @@ -19,16 +19,26 @@ export function initializeInfiniteScroll(pageType = 'loras') { switch (pageType) { case 'recipes': - loadMoreFunction = window.recipeManager?.loadMoreRecipes || (() => console.warn('loadMoreRecipes not found')); + loadMoreFunction = () => { + if (!pageState.isLoading && pageState.hasMore) { + pageState.currentPage++; + window.recipeManager.loadRecipes(false); // false to not reset pagination + } + }; gridId = 'recipeGrid'; break; case 'checkpoints': - loadMoreFunction = window.checkpointManager?.loadMoreCheckpoints || (() => console.warn('loadMoreCheckpoints not found')); + loadMoreFunction = () => { + if (!pageState.isLoading && pageState.hasMore) { + pageState.currentPage++; + window.checkpointManager.loadCheckpoints(false); // false to not reset pagination + } + }; gridId = 'checkpointGrid'; break; case 'loras': default: - loadMoreFunction = loadMoreLoras; + loadMoreFunction = () => loadMoreLoras(false); // false to not reset gridId = 'loraGrid'; break; }