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 = '
'; + if (!tagsContainer) return; + + tagsContainer.innerHTML = ''; + + // 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 = ''; + tagsContainer.innerHTML = ``; 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 = ''; + }); + } } 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 = '