From 93472e5d676b21c3311d57a3daed2e5b80ff8b72 Mon Sep 17 00:00:00 2001 From: Will Miao Date: Sat, 15 Aug 2026 09:50:46 +0800 Subject: [PATCH] feat(recipes): add sort by random option with seeded stable pagination --- py/services/recipe_scanner.py | 13 +- static/js/recipes.js | 54 +++- templates/components/controls.html | 8 +- .../pages/recipesPage.randomSort.test.js | 235 ++++++++++++++++++ tests/services/test_recipe_scanner.py | 48 ++++ 5 files changed, 348 insertions(+), 10 deletions(-) create mode 100644 tests/frontend/pages/recipesPage.randomSort.test.js diff --git a/py/services/recipe_scanner.py b/py/services/recipe_scanner.py index 72e82fc2..946bbdb2 100644 --- a/py/services/recipe_scanner.py +++ b/py/services/recipe_scanner.py @@ -8,6 +8,7 @@ import asyncio import json import logging import os +import random import time from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Union, cast from ..config import config @@ -2781,7 +2782,9 @@ class RecipeScanner: Args: page: Current page number (1-based) page_size: Number of items per page - sort_by: Sort method ('name' or 'date') + sort_by: Sort method ('name', 'date', 'loras_count', or 'random' + with an optional seed like 'random:abc123'; the part after + 'random:' is the shuffle seed, not a direction) search: Search term filters: Dictionary of filters to apply search_options: Dictionary of search options to apply @@ -2962,7 +2965,7 @@ class RecipeScanner: ] # Apply sorting if not already handled by pre-sorted cache - if ":" in sort_by or sort_field == "loras_count": + if ":" in sort_by or sort_field in ("loras_count", "random"): field, order = (sort_by.split(":") + ["desc"])[:2] reverse = order.lower() == "desc" @@ -2985,6 +2988,12 @@ class RecipeScanner: filtered_data.sort( key=lambda x: len(x.get("loras", [])), reverse=reverse ) + elif field == "random": + # Seeded random shuffle: same seed -> same order (stable + # pagination across requests), matching the model pages. + seed = order if order.lower() not in ("asc", "desc") else None + rng = random.Random(seed or "random") + rng.shuffle(filtered_data) # Calculate pagination total_items = len(filtered_data) diff --git a/static/js/recipes.js b/static/js/recipes.js index 47bec967..f6c5610a 100644 --- a/static/js/recipes.js +++ b/static/js/recipes.js @@ -245,10 +245,20 @@ class RecipeManager { this.pageState.sortBy = savedSort; } initSortDropdown(sortSelect); - sortSelect.value = this.pageState.sortBy || 'date:desc'; + this.applySortToSelect(this.pageState.sortBy || 'date:desc'); sortSelect.addEventListener('change', () => { - this.pageState.sortBy = sortSelect.value; - setStorageItem('recipes_sort', sortSelect.value); + let value = sortSelect.value; + if (value.startsWith('random')) { + // Every pick of Random reshuffles the list: generate a + // fresh seed so the backend keeps a stable order across + // paginated requests. + value = this._randomizeSortValue(); + } + this.pageState.sortBy = value; + setStorageItem('recipes_sort', value); + // Reset the seeded Random option when switching away from + // Random, or re-apply the fresh seed when picking it again. + this.applySortToSelect(value); refreshVirtualScroll(); }); } @@ -341,6 +351,44 @@ class RecipeManager { }); } + /** + * Apply a sort value to the native sort in sync so its value matches the persisted sort + * string and the dropdown shows the selected label. + * @returns {string} The new sort value, e.g. "random:abc123xyz" + */ + _randomizeSortValue() { + const seed = Math.random().toString(36).slice(2, 12); + const value = `random:${seed}`; + const sortSelect = document.getElementById('sortSelect'); + if (sortSelect) { + const randomOpt = sortSelect.querySelector('option[value="random"], option[value^="random:"]'); + if (randomOpt) { + randomOpt.value = value; + } + sortSelect.value = value; + } + return value; + } + normalizeLoadRecipesOptions(options = true) { if (typeof options === 'boolean') { return { diff --git a/templates/components/controls.html b/templates/components/controls.html index 7eb8a673..cf3f0cad 100644 --- a/templates/components/controls.html +++ b/templates/components/controls.html @@ -48,17 +48,15 @@ {% endif %} - {% if page_id != 'recipes' %} - - - - {% endif %} {% if page_id == 'recipes' %} {% endif %} + + +