From caf5b1528c14da32265754128c82a7cee02aac8d Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Thu, 20 Mar 2025 08:27:38 +0800 Subject: [PATCH] Enhance recipe search functionality with improved state management and search options - Introduced new search options for recipes, allowing users to filter by title, tags, LoRA filename, and LoRA model name. - Updated the RecipeRoutes and RecipeScanner to accommodate the new search options, enhancing the filtering capabilities. - Refactored RecipeManager and RecipeSearchManager to utilize the hierarchical state structure for managing search parameters and pagination state. - Improved the user interface by dynamically displaying relevant search options based on the current page context. --- py/routes/recipe_routes.py | 17 +++++- py/services/recipe_scanner.py | 46 +++++++++++++-- static/js/managers/RecipeSearchManager.js | 26 ++++---- static/js/managers/SearchManager.js | 10 +++- static/js/recipes.js | 72 +++++++++++++++-------- static/js/state/index.js | 13 ++-- templates/components/header.html | 22 ++++--- 7 files changed, 145 insertions(+), 61 deletions(-) diff --git a/py/routes/recipe_routes.py b/py/routes/recipe_routes.py index e5f7a298..9678105a 100644 --- a/py/routes/recipe_routes.py +++ b/py/routes/recipe_routes.py @@ -76,6 +76,12 @@ class RecipeRoutes: sort_by = request.query.get('sort_by', 'date') search = request.query.get('search', None) + # Get search options (renamed for better clarity) + search_title = request.query.get('search_title', 'true').lower() == 'true' + search_tags = request.query.get('search_tags', 'true').lower() == 'true' + search_lora_name = request.query.get('search_lora_name', 'true').lower() == 'true' + search_lora_model = request.query.get('search_lora_model', 'true').lower() == 'true' + # Get filter parameters base_models = request.query.get('base_models', None) tags = request.query.get('tags', None) @@ -87,13 +93,22 @@ class RecipeRoutes: if tags: filters['tags'] = tags.split(',') + # Add search options to filters + search_options = { + 'title': search_title, + 'tags': search_tags, + 'lora_name': search_lora_name, + 'lora_model': search_lora_model + } + # Get paginated data result = await self.recipe_scanner.get_paginated_data( page=page, page_size=page_size, sort_by=sort_by, search=search, - filters=filters + filters=filters, + search_options=search_options ) # Format the response data with static URLs for file paths diff --git a/py/services/recipe_scanner.py b/py/services/recipe_scanner.py index 196f7189..b99491a1 100644 --- a/py/services/recipe_scanner.py +++ b/py/services/recipe_scanner.py @@ -348,7 +348,7 @@ class RecipeScanner: logger.error(f"Error getting base model for lora: {e}") return None - async def get_paginated_data(self, page: int, page_size: int, sort_by: str = 'date', search: str = None, filters: dict = None): + async def get_paginated_data(self, page: int, page_size: int, sort_by: str = 'date', search: str = None, filters: dict = None, search_options: dict = None): """Get paginated and filtered recipe data Args: @@ -357,6 +357,7 @@ class RecipeScanner: sort_by: Sort method ('name' or 'date') search: Search term filters: Dictionary of filters to apply + search_options: Dictionary of search options to apply """ cache = await self.get_cached_data() @@ -365,11 +366,44 @@ class RecipeScanner: # Apply search filter if search: - filtered_data = [ - item for item in filtered_data - if search.lower() in str(item.get('title', '')).lower() or - search.lower() in str(item.get('prompt', '')).lower() - ] + # Default search options if none provided + if not search_options: + search_options = { + 'title': True, + 'tags': True, + 'lora_name': True, + 'lora_model': True + } + + # Build the search predicate based on search options + def matches_search(item): + # Search in title if enabled + if search_options.get('title', True) and search.lower() in str(item.get('title', '')).lower(): + return True + + # Search in tags if enabled + if search_options.get('tags', True) and 'tags' in item: + for tag in item['tags']: + if search.lower() in tag.lower(): + return True + + # Search in lora file names if enabled + if search_options.get('lora_name', True) and 'loras' in item: + for lora in item['loras']: + if search.lower() in str(lora.get('file_name', '')).lower(): + return True + + # Search in lora model names if enabled + if search_options.get('lora_model', True) and 'loras' in item: + for lora in item['loras']: + if search.lower() in str(lora.get('modelName', '')).lower(): + return True + + # No match found + return False + + # Filter the data using the search predicate + filtered_data = [item for item in filtered_data if matches_search(item)] # Apply additional filters if filters: diff --git a/static/js/managers/RecipeSearchManager.js b/static/js/managers/RecipeSearchManager.js index dac6ee88..13de63d7 100644 --- a/static/js/managers/RecipeSearchManager.js +++ b/static/js/managers/RecipeSearchManager.js @@ -3,7 +3,7 @@ * Extends the base SearchManager with recipe-specific functionality */ import { SearchManager } from './SearchManager.js'; -import { state } from '../state/index.js'; +import { state, getCurrentPageState } from '../state/index.js'; import { showToast } from '../utils/uiHelpers.js'; export class RecipeSearchManager extends SearchManager { @@ -17,7 +17,7 @@ export class RecipeSearchManager extends SearchManager { // Store this instance in the state if (state) { - state.searchManager = this; + state.pages.recipes.searchManager = this; } } @@ -34,7 +34,7 @@ export class RecipeSearchManager extends SearchManager { if (!searchTerm) { if (state) { - state.currentPage = 1; + state.pages.recipes.currentPage = 1; } this.resetAndReloadRecipes(); return; @@ -50,22 +50,24 @@ export class RecipeSearchManager extends SearchManager { const scrollPosition = window.pageYOffset || document.documentElement.scrollTop; if (state) { - state.currentPage = 1; - state.hasMore = true; + 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.sortBy : 'name'); + 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 searchOptions = this.getActiveSearchOptions(); - url.searchParams.set('search_name', searchOptions.modelname.toString()); + 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_loras', searchOptions.loras.toString()); + url.searchParams.set('search_lora_name', searchOptions.loraName.toString()); + url.searchParams.set('search_lora_model', searchOptions.loraModel.toString()); const response = await fetch(url); @@ -81,13 +83,13 @@ export class RecipeSearchManager extends SearchManager { if (data.items.length === 0) { grid.innerHTML = '
No matching recipes found
'; if (state) { - state.hasMore = false; + state.pages.recipes.hasMore = false; } } else { this.appendRecipeCards(data.items); if (state) { - state.hasMore = state.currentPage < data.total_pages; - state.currentPage++; + state.pages.recipes.hasMore = state.pages.recipes.currentPage < data.total_pages; + state.pages.recipes.currentPage++; } } diff --git a/static/js/managers/SearchManager.js b/static/js/managers/SearchManager.js index 17259883..54628a43 100644 --- a/static/js/managers/SearchManager.js +++ b/static/js/managers/SearchManager.js @@ -194,7 +194,7 @@ export class SearchManager { }); } - // Apply recursive search + // Apply recursive search - only if the toggle exists if (this.recursiveSearchToggle && preferences.recursive !== undefined) { this.recursiveSearchToggle.checked = preferences.recursive; } @@ -245,10 +245,14 @@ export class SearchManager { }); const preferences = { - options, - recursive: this.recursiveSearchToggle ? this.recursiveSearchToggle.checked : false + options }; + // Only add recursive option if the toggle exists + if (this.recursiveSearchToggle) { + preferences.recursive = this.recursiveSearchToggle.checked; + } + localStorage.setItem(`${this.currentPage}_search_prefs`, JSON.stringify(preferences)); } catch (error) { console.error('Error saving search preferences:', error); diff --git a/static/js/recipes.js b/static/js/recipes.js index 5d13421a..db474bb0 100644 --- a/static/js/recipes.js +++ b/static/js/recipes.js @@ -3,13 +3,15 @@ import { appCore } from './core.js'; import { ImportManager } from './managers/ImportManager.js'; import { RecipeCard } from './components/RecipeCard.js'; import { RecipeModal } from './components/RecipeModal.js'; +import { state, getCurrentPageState, setCurrentPageType, initPageState } from './state/index.js'; class RecipeManager { constructor() { - this.currentPage = 1; - this.pageSize = 20; - this.sortBy = 'date'; - this.filterParams = {}; + // Initialize recipe page state + initPageState('recipes'); + + // Get page state + this.pageState = getCurrentPageState(); // Initialize ImportManager this.importManager = new ImportManager(); @@ -18,14 +20,17 @@ class RecipeManager { this.recipeModal = new RecipeModal(); // Add state tracking for infinite scroll - this.isLoading = false; - this.hasMore = true; + this.pageState.isLoading = false; + this.pageState.hasMore = true; } async initialize() { // Initialize event listeners this.initEventListeners(); + // Set default search options if not already defined + this._initSearchOptions(); + // Load initial set of recipes await this.loadRecipes(); @@ -36,6 +41,18 @@ class RecipeManager { appCore.initializePageFeatures(); } + _initSearchOptions() { + // Ensure recipes search options are properly initialized + if (!this.pageState.searchOptions) { + this.pageState.searchOptions = { + title: true, // Recipe title + tags: true, // Recipe tags + loraName: true, // LoRA file name + loraModel: true // LoRA model name + }; + } + } + _exposeGlobalFunctions() { // Only expose what's needed for the page window.recipeManager = this; @@ -49,7 +66,7 @@ class RecipeManager { const sortSelect = document.getElementById('sortSelect'); if (sortSelect) { sortSelect.addEventListener('change', () => { - this.sortBy = sortSelect.value; + this.pageState.sortBy = sortSelect.value; this.loadRecipes(); }); } @@ -61,7 +78,7 @@ class RecipeManager { searchInput.addEventListener('input', () => { clearTimeout(debounceTimeout); debounceTimeout = setTimeout(() => { - this.filterParams.search = searchInput.value; + this.pageState.filters.search = searchInput.value; this.loadRecipes(); }, 300); }); @@ -81,11 +98,11 @@ class RecipeManager { try { // Show loading indicator document.body.classList.add('loading'); - this.isLoading = true; + this.pageState.isLoading = true; // Reset to first page if requested if (resetPage) { - this.currentPage = 1; + this.pageState.currentPage = 1; // Clear grid if resetting const grid = document.getElementById('recipeGrid'); if (grid) grid.innerHTML = ''; @@ -93,19 +110,24 @@ class RecipeManager { // Build query parameters const params = new URLSearchParams({ - page: this.currentPage, - page_size: this.pageSize, - sort_by: this.sortBy + page: this.pageState.currentPage, + page_size: this.pageState.pageSize || 20, + sort_by: this.pageState.sortBy }); // Add search filter if present - if (this.filterParams.search) { - params.append('search', this.filterParams.search); + if (this.pageState.filters.search) { + params.append('search', this.pageState.filters.search); } - // Add other filters - if (this.filterParams.baseModels && this.filterParams.baseModels.length) { - params.append('base_models', this.filterParams.baseModels.join(',')); + // Add base model filters + if (this.pageState.filters.baseModel && this.pageState.filters.baseModel.length) { + params.append('base_models', this.pageState.filters.baseModel.join(',')); + } + + // Add tag filters + if (this.pageState.filters.tags && this.pageState.filters.tags.length) { + params.append('tags', this.pageState.filters.tags.join(',')); } // Fetch recipes @@ -121,7 +143,7 @@ class RecipeManager { this.updateRecipesGrid(data, resetPage); // Update pagination state - this.hasMore = data.has_more || false; + this.pageState.hasMore = data.has_more || false; } catch (error) { console.error('Error loading recipes:', error); @@ -129,15 +151,15 @@ class RecipeManager { } finally { // Hide loading indicator document.body.classList.remove('loading'); - this.isLoading = false; + this.pageState.isLoading = false; } } // Load more recipes for infinite scroll async loadMoreRecipes() { - if (this.isLoading || !this.hasMore) return; + if (this.pageState.isLoading || !this.pageState.hasMore) return; - this.currentPage++; + this.pageState.currentPage++; await this.loadRecipes(false); } @@ -170,7 +192,7 @@ class RecipeManager { }); // Add sentinel for infinite scroll if needed - if (this.hasMore) { + if (this.pageState.hasMore) { let sentinel = document.getElementById('scroll-sentinel'); if (!sentinel) { sentinel = document.createElement('div'); @@ -179,8 +201,8 @@ class RecipeManager { grid.appendChild(sentinel); // Re-observe the sentinel if we have an observer - if (window.state && window.state.observer) { - window.state.observer.observe(sentinel); + if (state && state.observer) { + state.observer.observe(sentinel); } } } diff --git a/static/js/state/index.js b/static/js/state/index.js index 1a54a7d0..eeeb699b 100644 --- a/static/js/state/index.js +++ b/static/js/state/index.js @@ -42,16 +42,17 @@ export const state = { sortBy: 'date', searchManager: null, searchOptions: { - filename: true, - modelname: true, + title: true, tags: true, - loras: true, - recursive: false + loraName: true, + loraModel: true }, filters: { baseModel: [], - tags: [] - } + tags: [], + search: '' + }, + pageSize: 20 }, checkpoints: { diff --git a/templates/components/header.html b/templates/components/header.html index 7a07dd49..f9ddc624 100644 --- a/templates/components/header.html +++ b/templates/components/header.html @@ -66,18 +66,23 @@

Search In:

-
Filename
- {% if request.path == '/loras' or request.path == '/loras/recipes' %} -
Tags
- {% endif %} -
- {% if request.path == '/loras/recipes' %}Recipe Name{% elif request.path == '/checkpoints' %}Checkpoint Name{% else %}Model Name{% endif %} -
{% if request.path == '/loras/recipes' %} -
Included LoRAs
+
Recipe Title
+
Tags
+
LoRA Filename
+
LoRA Model Name
+ {% elif request.path == '/checkpoints' %} +
Filename
+
Checkpoint Name
+ {% else %} + +
Filename
+
Model Name
+
Tags
{% endif %}
+ {% if request.path != '/loras/recipes' %}
Include Subfolders @@ -87,6 +92,7 @@
+ {% endif %}