feat: add folder-based recipe organization and navigation

- Add new API endpoints for folder operations: get_folders, get_folder_tree, and get_unified_folder_tree
- Extend recipe listing handler to support folder and recursive filtering parameters
- Register new folder-related routes in route definitions
- Enable users to organize and browse recipes using folder structures
This commit is contained in:
Will Miao
2025-11-25 11:10:58 +08:00
parent dd89aa49c1
commit 67fb205b43
9 changed files with 303 additions and 21 deletions

View File

@@ -56,6 +56,9 @@ class RecipeHandlerSet:
"delete_recipe": self.management.delete_recipe, "delete_recipe": self.management.delete_recipe,
"get_top_tags": self.query.get_top_tags, "get_top_tags": self.query.get_top_tags,
"get_base_models": self.query.get_base_models, "get_base_models": self.query.get_base_models,
"get_folders": self.query.get_folders,
"get_folder_tree": self.query.get_folder_tree,
"get_unified_folder_tree": self.query.get_unified_folder_tree,
"share_recipe": self.sharing.share_recipe, "share_recipe": self.sharing.share_recipe,
"download_shared_recipe": self.sharing.download_shared_recipe, "download_shared_recipe": self.sharing.download_shared_recipe,
"get_recipe_syntax": self.query.get_recipe_syntax, "get_recipe_syntax": self.query.get_recipe_syntax,
@@ -149,6 +152,8 @@ class RecipeListingHandler:
page_size = int(request.query.get("page_size", "20")) page_size = int(request.query.get("page_size", "20"))
sort_by = request.query.get("sort_by", "date") sort_by = request.query.get("sort_by", "date")
search = request.query.get("search") search = request.query.get("search")
folder = request.query.get("folder")
recursive = request.query.get("recursive", "true").lower() == "true"
search_options = { search_options = {
"title": request.query.get("search_title", "true").lower() == "true", "title": request.query.get("search_title", "true").lower() == "true",
@@ -193,6 +198,8 @@ class RecipeListingHandler:
filters=filters, filters=filters,
search_options=search_options, search_options=search_options,
lora_hash=lora_hash, lora_hash=lora_hash,
folder=folder,
recursive=recursive,
) )
for item in result.get("items", []): for item in result.get("items", []):
@@ -299,6 +306,45 @@ class RecipeQueryHandler:
self._logger.error("Error retrieving base models: %s", exc, exc_info=True) self._logger.error("Error retrieving base models: %s", exc, exc_info=True)
return web.json_response({"success": False, "error": str(exc)}, status=500) return web.json_response({"success": False, "error": str(exc)}, status=500)
async def get_folders(self, request: web.Request) -> web.Response:
try:
await self._ensure_dependencies_ready()
recipe_scanner = self._recipe_scanner_getter()
if recipe_scanner is None:
raise RuntimeError("Recipe scanner unavailable")
folders = await recipe_scanner.get_folders()
return web.json_response({"success": True, "folders": folders})
except Exception as exc:
self._logger.error("Error retrieving recipe folders: %s", exc, exc_info=True)
return web.json_response({"success": False, "error": str(exc)}, status=500)
async def get_folder_tree(self, request: web.Request) -> web.Response:
try:
await self._ensure_dependencies_ready()
recipe_scanner = self._recipe_scanner_getter()
if recipe_scanner is None:
raise RuntimeError("Recipe scanner unavailable")
folder_tree = await recipe_scanner.get_folder_tree()
return web.json_response({"success": True, "tree": folder_tree})
except Exception as exc:
self._logger.error("Error retrieving recipe folder tree: %s", exc, exc_info=True)
return web.json_response({"success": False, "error": str(exc)}, status=500)
async def get_unified_folder_tree(self, request: web.Request) -> web.Response:
try:
await self._ensure_dependencies_ready()
recipe_scanner = self._recipe_scanner_getter()
if recipe_scanner is None:
raise RuntimeError("Recipe scanner unavailable")
folder_tree = await recipe_scanner.get_folder_tree()
return web.json_response({"success": True, "tree": folder_tree})
except Exception as exc:
self._logger.error("Error retrieving unified recipe folder tree: %s", exc, exc_info=True)
return web.json_response({"success": False, "error": str(exc)}, status=500)
async def get_recipes_for_lora(self, request: web.Request) -> web.Response: async def get_recipes_for_lora(self, request: web.Request) -> web.Response:
try: try:
await self._ensure_dependencies_ready() await self._ensure_dependencies_ready()

View File

@@ -27,6 +27,9 @@ ROUTE_DEFINITIONS: tuple[RouteDefinition, ...] = (
RouteDefinition("DELETE", "/api/lm/recipe/{recipe_id}", "delete_recipe"), RouteDefinition("DELETE", "/api/lm/recipe/{recipe_id}", "delete_recipe"),
RouteDefinition("GET", "/api/lm/recipes/top-tags", "get_top_tags"), RouteDefinition("GET", "/api/lm/recipes/top-tags", "get_top_tags"),
RouteDefinition("GET", "/api/lm/recipes/base-models", "get_base_models"), RouteDefinition("GET", "/api/lm/recipes/base-models", "get_base_models"),
RouteDefinition("GET", "/api/lm/recipes/folders", "get_folders"),
RouteDefinition("GET", "/api/lm/recipes/folder-tree", "get_folder_tree"),
RouteDefinition("GET", "/api/lm/recipes/unified-folder-tree", "get_unified_folder_tree"),
RouteDefinition("GET", "/api/lm/recipe/{recipe_id}/share", "share_recipe"), RouteDefinition("GET", "/api/lm/recipe/{recipe_id}/share", "share_recipe"),
RouteDefinition("GET", "/api/lm/recipe/{recipe_id}/share/download", "download_shared_recipe"), RouteDefinition("GET", "/api/lm/recipe/{recipe_id}/share/download", "download_shared_recipe"),
RouteDefinition("GET", "/api/lm/recipe/{recipe_id}/syntax", "get_recipe_syntax"), RouteDefinition("GET", "/api/lm/recipe/{recipe_id}/syntax", "get_recipe_syntax"),

View File

@@ -7,12 +7,18 @@ from natsort import natsorted
@dataclass @dataclass
class RecipeCache: class RecipeCache:
"""Cache structure for Recipe data""" """Cache structure for Recipe data"""
raw_data: List[Dict] raw_data: List[Dict]
sorted_by_name: List[Dict] sorted_by_name: List[Dict]
sorted_by_date: List[Dict] sorted_by_date: List[Dict]
folders: List[str] | None = None
folder_tree: Dict | None = None
def __post_init__(self): def __post_init__(self):
self._lock = asyncio.Lock() self._lock = asyncio.Lock()
# Normalize optional metadata containers
self.folders = self.folders or []
self.folder_tree = self.folder_tree or {}
async def resort(self, name_only: bool = False): async def resort(self, name_only: bool = False):
"""Resort all cached data views""" """Resort all cached data views"""

View File

@@ -1,7 +1,9 @@
import os from __future__ import annotations
import logging
import asyncio import asyncio
import json import json
import logging
import os
import time import time
from typing import Any, Dict, Iterable, List, Optional, Set, Tuple from typing import Any, Dict, Iterable, List, Optional, Set, Tuple
from ..config import config from ..config import config
@@ -117,7 +119,9 @@ class RecipeScanner:
self._cache = RecipeCache( self._cache = RecipeCache(
raw_data=[], raw_data=[],
sorted_by_name=[], sorted_by_name=[],
sorted_by_date=[] sorted_by_date=[],
folders=[],
folder_tree={},
) )
# Mark as initializing to prevent concurrent initializations # Mark as initializing to prevent concurrent initializations
@@ -218,6 +222,7 @@ class RecipeScanner:
# Update cache with the collected data # Update cache with the collected data
self._cache.raw_data = recipes self._cache.raw_data = recipes
self._update_folder_metadata(self._cache)
# Create a simplified resort function that doesn't use await # Create a simplified resort function that doesn't use await
if hasattr(self._cache, "resort"): if hasattr(self._cache, "resort"):
@@ -336,6 +341,9 @@ class RecipeScanner:
if not self._cache: if not self._cache:
return return
# Keep folder metadata up to date alongside sort order
self._update_folder_metadata()
async def _resort_wrapper() -> None: async def _resort_wrapper() -> None:
try: try:
await self._cache.resort(name_only=name_only) await self._cache.resort(name_only=name_only)
@@ -346,6 +354,75 @@ class RecipeScanner:
self._resort_tasks.add(task) self._resort_tasks.add(task)
task.add_done_callback(lambda finished: self._resort_tasks.discard(finished)) task.add_done_callback(lambda finished: self._resort_tasks.discard(finished))
def _calculate_folder(self, recipe_path: str) -> str:
"""Calculate a normalized folder path relative to ``recipes_dir``."""
recipes_dir = self.recipes_dir
if not recipes_dir:
return ""
try:
recipe_dir = os.path.dirname(os.path.normpath(recipe_path))
relative_dir = os.path.relpath(recipe_dir, recipes_dir)
if relative_dir in (".", ""):
return ""
return relative_dir.replace(os.path.sep, "/")
except Exception:
return ""
def _build_folder_tree(self, folders: list[str]) -> dict:
"""Build a nested folder tree structure from relative folder paths."""
tree: dict[str, dict] = {}
for folder in folders:
if not folder:
continue
parts = folder.split("/")
current_level = tree
for part in parts:
if part not in current_level:
current_level[part] = {}
current_level = current_level[part]
return tree
def _update_folder_metadata(self, cache: RecipeCache | None = None) -> None:
"""Ensure folder lists and tree metadata are synchronized with cache contents."""
cache = cache or self._cache
if cache is None:
return
folders: set[str] = set()
for item in cache.raw_data:
folder_value = item.get("folder", "")
if folder_value is None:
folder_value = ""
if folder_value == ".":
folder_value = ""
normalized = str(folder_value).replace("\\", "/")
item["folder"] = normalized
folders.add(normalized)
cache.folders = sorted(folders, key=lambda entry: entry.lower())
cache.folder_tree = self._build_folder_tree(cache.folders)
async def get_folders(self) -> list[str]:
"""Return a sorted list of recipe folders relative to the recipes root."""
cache = await self.get_cached_data()
self._update_folder_metadata(cache)
return cache.folders
async def get_folder_tree(self) -> dict:
"""Return a hierarchical tree of recipe folders for sidebar navigation."""
cache = await self.get_cached_data()
self._update_folder_metadata(cache)
return cache.folder_tree
@property @property
def recipes_dir(self) -> str: def recipes_dir(self) -> str:
"""Get path to recipes directory""" """Get path to recipes directory"""
@@ -362,11 +439,14 @@ class RecipeScanner:
"""Get cached recipe data, refresh if needed""" """Get cached recipe data, refresh if needed"""
# If cache is already initialized and no refresh is needed, return it immediately # If cache is already initialized and no refresh is needed, return it immediately
if self._cache is not None and not force_refresh: if self._cache is not None and not force_refresh:
self._update_folder_metadata()
return self._cache return self._cache
# If another initialization is already in progress, wait for it to complete # If another initialization is already in progress, wait for it to complete
if self._is_initializing and not force_refresh: if self._is_initializing and not force_refresh:
return self._cache or RecipeCache(raw_data=[], sorted_by_name=[], sorted_by_date=[]) return self._cache or RecipeCache(
raw_data=[], sorted_by_name=[], sorted_by_date=[], folders=[], folder_tree={}
)
# If force refresh is requested, initialize the cache directly # If force refresh is requested, initialize the cache directly
if force_refresh: if force_refresh:
@@ -384,11 +464,14 @@ class RecipeScanner:
self._cache = RecipeCache( self._cache = RecipeCache(
raw_data=raw_data, raw_data=raw_data,
sorted_by_name=[], sorted_by_name=[],
sorted_by_date=[] sorted_by_date=[],
folders=[],
folder_tree={},
) )
# Resort cache # Resort cache
await self._cache.resort() await self._cache.resort()
self._update_folder_metadata(self._cache)
return self._cache return self._cache
@@ -398,7 +481,9 @@ class RecipeScanner:
self._cache = RecipeCache( self._cache = RecipeCache(
raw_data=[], raw_data=[],
sorted_by_name=[], sorted_by_name=[],
sorted_by_date=[] sorted_by_date=[],
folders=[],
folder_tree={},
) )
return self._cache return self._cache
finally: finally:
@@ -409,7 +494,9 @@ class RecipeScanner:
logger.error(f"Unexpected error in get_cached_data: {e}") logger.error(f"Unexpected error in get_cached_data: {e}")
# Return the cache (may be empty or partially initialized) # Return the cache (may be empty or partially initialized)
return self._cache or RecipeCache(raw_data=[], sorted_by_name=[], sorted_by_date=[]) return self._cache or RecipeCache(
raw_data=[], sorted_by_name=[], sorted_by_date=[], folders=[], folder_tree={}
)
async def refresh_cache(self, force: bool = False) -> RecipeCache: async def refresh_cache(self, force: bool = False) -> RecipeCache:
"""Public helper to refresh or return the recipe cache.""" """Public helper to refresh or return the recipe cache."""
@@ -424,6 +511,7 @@ class RecipeScanner:
cache = await self.get_cached_data() cache = await self.get_cached_data()
await cache.add_recipe(recipe_data, resort=False) await cache.add_recipe(recipe_data, resort=False)
self._update_folder_metadata(cache)
self._schedule_resort() self._schedule_resort()
async def remove_recipe(self, recipe_id: str) -> bool: async def remove_recipe(self, recipe_id: str) -> bool:
@@ -437,6 +525,7 @@ class RecipeScanner:
if removed is None: if removed is None:
return False return False
self._update_folder_metadata(cache)
self._schedule_resort() self._schedule_resort()
return True return True
@@ -521,6 +610,9 @@ class RecipeScanner:
if path_updated: if path_updated:
self._write_recipe_file(recipe_path, recipe_data) self._write_recipe_file(recipe_path, recipe_data)
# Track folder placement relative to recipes directory
recipe_data['folder'] = recipe_data.get('folder') or self._calculate_folder(recipe_path)
# Ensure loras array exists # Ensure loras array exists
if 'loras' not in recipe_data: if 'loras' not in recipe_data:
@@ -914,7 +1006,7 @@ class RecipeScanner:
return await self._lora_scanner.get_model_info_by_name(name) return await self._lora_scanner.get_model_info_by_name(name)
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, lora_hash: str = None, bypass_filters: bool = True): 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, lora_hash: str = None, bypass_filters: bool = True, folder: str | None = None, recursive: bool = True):
"""Get paginated and filtered recipe data """Get paginated and filtered recipe data
Args: Args:
@@ -926,6 +1018,8 @@ class RecipeScanner:
search_options: Dictionary of search options to apply search_options: Dictionary of search options to apply
lora_hash: Optional SHA256 hash of a LoRA to filter recipes by lora_hash: Optional SHA256 hash of a LoRA to filter recipes by
bypass_filters: If True, ignore other filters when a lora_hash is provided bypass_filters: If True, ignore other filters when a lora_hash is provided
folder: Optional folder filter relative to recipes directory
recursive: Whether to include recipes in subfolders of the selected folder
""" """
cache = await self.get_cached_data() cache = await self.get_cached_data()
@@ -961,6 +1055,22 @@ class RecipeScanner:
# Skip further filtering if we're only filtering by LoRA hash with bypass enabled # Skip further filtering if we're only filtering by LoRA hash with bypass enabled
if not (lora_hash and bypass_filters): if not (lora_hash and bypass_filters):
# Apply folder filter before other criteria
normalized_folder = (folder or "").strip("/")
if normalized_folder:
def matches_folder(item_folder: str) -> bool:
item_path = (item_folder or "").strip("/")
if not item_path:
return False
if recursive:
return item_path == normalized_folder or item_path.startswith(f"{normalized_folder}/")
return item_path == normalized_folder
filtered_data = [
item for item in filtered_data
if matches_folder(item.get('folder', ''))
]
# Apply search filter # Apply search filter
if search: if search:
# Default search options if none provided # Default search options if none provided

View File

@@ -2,6 +2,24 @@ import { RecipeCard } from '../components/RecipeCard.js';
import { state, getCurrentPageState } from '../state/index.js'; import { state, getCurrentPageState } from '../state/index.js';
import { showToast } from '../utils/uiHelpers.js'; import { showToast } from '../utils/uiHelpers.js';
const RECIPE_ENDPOINTS = {
list: '/api/lm/recipes',
detail: '/api/lm/recipe',
scan: '/api/lm/recipes/scan',
update: '/api/lm/recipe',
folders: '/api/lm/recipes/folders',
folderTree: '/api/lm/recipes/folder-tree',
unifiedFolderTree: '/api/lm/recipes/unified-folder-tree',
};
const RECIPE_SIDEBAR_CONFIG = {
config: {
displayName: 'Recipes',
supportsMove: false,
},
endpoints: RECIPE_ENDPOINTS,
};
/** /**
* Fetch recipes with pagination for virtual scrolling * Fetch recipes with pagination for virtual scrolling
* @param {number} page - Page number to fetch * @param {number} page - Page number to fetch
@@ -17,11 +35,18 @@ export async function fetchRecipesPage(page = 1, pageSize = 100) {
page_size: pageSize || pageState.pageSize || 20, page_size: pageSize || pageState.pageSize || 20,
sort_by: pageState.sortBy sort_by: pageState.sortBy
}); });
if (pageState.activeFolder) {
params.append('folder', pageState.activeFolder);
params.append('recursive', pageState.searchOptions?.recursive !== false);
} else if (pageState.searchOptions?.recursive !== undefined) {
params.append('recursive', pageState.searchOptions.recursive);
}
// If we have a specific recipe ID to load // If we have a specific recipe ID to load
if (pageState.customFilter?.active && pageState.customFilter?.recipeId) { if (pageState.customFilter?.active && pageState.customFilter?.recipeId) {
// Special case: load specific recipe // Special case: load specific recipe
const response = await fetch(`/api/lm/recipe/${pageState.customFilter.recipeId}`); const response = await fetch(`${RECIPE_ENDPOINTS.detail}/${pageState.customFilter.recipeId}`);
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to load recipe: ${response.statusText}`); throw new Error(`Failed to load recipe: ${response.statusText}`);
@@ -78,7 +103,7 @@ export async function fetchRecipesPage(page = 1, pageSize = 100) {
} }
// Fetch recipes // Fetch recipes
const response = await fetch(`/api/lm/recipes?${params.toString()}`); const response = await fetch(`${RECIPE_ENDPOINTS.list}?${params.toString()}`);
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to load recipes: ${response.statusText}`); throw new Error(`Failed to load recipes: ${response.statusText}`);
@@ -213,7 +238,7 @@ export async function refreshRecipes() {
state.loadingManager.showSimpleLoading('Refreshing recipes...'); state.loadingManager.showSimpleLoading('Refreshing recipes...');
// Call the API endpoint to rebuild the recipe cache // Call the API endpoint to rebuild the recipe cache
const response = await fetch('/api/lm/recipes/scan'); const response = await fetch(RECIPE_ENDPOINTS.scan);
if (!response.ok) { if (!response.ok) {
const data = await response.json(); const data = await response.json();
@@ -280,7 +305,7 @@ export async function updateRecipeMetadata(filePath, updates) {
const basename = filePath.split('/').pop().split('\\').pop(); const basename = filePath.split('/').pop().split('\\').pop();
const recipeId = basename.substring(0, basename.lastIndexOf('.')); const recipeId = basename.substring(0, basename.lastIndexOf('.'));
const response = await fetch(`/api/lm/recipe/${recipeId}/update`, { const response = await fetch(`${RECIPE_ENDPOINTS.update}/${recipeId}/update`, {
method: 'PUT', method: 'PUT',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -306,3 +331,33 @@ export async function updateRecipeMetadata(filePath, updates) {
state.loadingManager.hide(); state.loadingManager.hide();
} }
} }
export class RecipeSidebarApiClient {
constructor() {
this.apiConfig = RECIPE_SIDEBAR_CONFIG;
}
async fetchUnifiedFolderTree() {
const response = await fetch(this.apiConfig.endpoints.unifiedFolderTree);
if (!response.ok) {
throw new Error('Failed to fetch recipe folder tree');
}
return response.json();
}
async fetchModelFolders() {
const response = await fetch(this.apiConfig.endpoints.folders);
if (!response.ok) {
throw new Error('Failed to fetch recipe folders');
}
return response.json();
}
async moveBulkModels() {
throw new Error('Recipe move operations are not supported.');
}
async moveSingleModel() {
throw new Error('Recipe move operations are not supported.');
}
}

View File

@@ -77,7 +77,9 @@ export class SidebarManager {
this.pageControls = pageControls; this.pageControls = pageControls;
this.pageType = pageControls.pageType; this.pageType = pageControls.pageType;
this.lastPageControls = pageControls; this.lastPageControls = pageControls;
this.apiClient = getModelApiClient(); this.apiClient = pageControls?.getSidebarApiClient?.()
|| pageControls?.sidebarApiClient
|| getModelApiClient();
// Set initial sidebar state immediately (hidden by default) // Set initial sidebar state immediately (hidden by default)
this.setInitialSidebarState(); this.setInitialSidebarState();
@@ -205,6 +207,10 @@ export class SidebarManager {
} }
initializeDragAndDrop() { initializeDragAndDrop() {
if (this.apiClient?.apiConfig?.config?.supportsMove === false) {
return;
}
if (!this.dragHandlersInitialized) { if (!this.dragHandlersInitialized) {
document.addEventListener('dragstart', this.handleCardDragStart); document.addEventListener('dragstart', this.handleCardDragStart);
document.addEventListener('dragend', this.handleCardDragEnd); document.addEventListener('dragend', this.handleCardDragEnd);
@@ -416,7 +422,14 @@ export class SidebarManager {
} }
if (!this.apiClient) { if (!this.apiClient) {
this.apiClient = getModelApiClient(); this.apiClient = this.pageControls?.getSidebarApiClient?.()
|| this.pageControls?.sidebarApiClient
|| getModelApiClient();
}
if (this.apiClient?.apiConfig?.config?.supportsMove === false) {
showToast('toast.models.moveFailed', { message: translate('sidebar.dragDrop.moveUnsupported', {}, 'Move not supported for this page') }, 'error');
return false;
} }
const rootPath = this.draggedRootPath ? this.draggedRootPath.replace(/\\/g, '/') : ''; const rootPath = this.draggedRootPath ? this.draggedRootPath.replace(/\\/g, '/') : '';
@@ -470,7 +483,9 @@ export class SidebarManager {
} }
async init() { async init() {
this.apiClient = getModelApiClient(); this.apiClient = this.pageControls?.getSidebarApiClient?.()
|| this.pageControls?.sidebarApiClient
|| getModelApiClient();
// Set initial sidebar state immediately (hidden by default) // Set initial sidebar state immediately (hidden by default)
this.setInitialSidebarState(); this.setInitialSidebarState();

View File

@@ -2,17 +2,46 @@
import { appCore } from './core.js'; import { appCore } from './core.js';
import { ImportManager } from './managers/ImportManager.js'; import { ImportManager } from './managers/ImportManager.js';
import { RecipeModal } from './components/RecipeModal.js'; import { RecipeModal } from './components/RecipeModal.js';
import { getCurrentPageState } from './state/index.js'; import { state, getCurrentPageState } from './state/index.js';
import { getSessionItem, removeSessionItem } from './utils/storageHelpers.js'; import { getSessionItem, removeSessionItem } from './utils/storageHelpers.js';
import { RecipeContextMenu } from './components/ContextMenu/index.js'; import { RecipeContextMenu } from './components/ContextMenu/index.js';
import { DuplicatesManager } from './components/DuplicatesManager.js'; import { DuplicatesManager } from './components/DuplicatesManager.js';
import { refreshVirtualScroll } from './utils/infiniteScroll.js'; import { refreshVirtualScroll } from './utils/infiniteScroll.js';
import { refreshRecipes } from './api/recipeApi.js'; import { refreshRecipes, RecipeSidebarApiClient } from './api/recipeApi.js';
import { sidebarManager } from './components/SidebarManager.js';
class RecipePageControls {
constructor() {
this.pageType = 'recipes';
this.pageState = getCurrentPageState();
this.sidebarApiClient = new RecipeSidebarApiClient();
}
async resetAndReload() {
refreshVirtualScroll();
}
async refreshModels(fullRebuild = false) {
if (fullRebuild) {
await refreshRecipes();
return;
}
refreshVirtualScroll();
}
getSidebarApiClient() {
return this.sidebarApiClient;
}
}
class RecipeManager { class RecipeManager {
constructor() { constructor() {
// Get page state // Get page state
this.pageState = getCurrentPageState(); this.pageState = getCurrentPageState();
// Page controls for shared sidebar behaviors
this.pageControls = new RecipePageControls();
// Initialize ImportManager // Initialize ImportManager
this.importManager = new ImportManager(); this.importManager = new ImportManager();
@@ -51,10 +80,23 @@ class RecipeManager {
// Expose necessary functions to the page // Expose necessary functions to the page
this._exposeGlobalFunctions(); this._exposeGlobalFunctions();
// Initialize sidebar navigation
await this._initSidebar();
// Initialize common page features // Initialize common page features
appCore.initializePageFeatures(); appCore.initializePageFeatures();
} }
async _initSidebar() {
try {
sidebarManager.setHostPageControls(this.pageControls);
const shouldShowSidebar = state?.global?.settings?.show_folder_sidebar !== false;
await sidebarManager.setSidebarEnabled(shouldShowSidebar);
} catch (error) {
console.error('Failed to initialize recipe sidebar:', error);
}
}
_initSearchOptions() { _initSearchOptions() {
// Ensure recipes search options are properly initialized // Ensure recipes search options are properly initialized
@@ -63,7 +105,8 @@ class RecipeManager {
title: true, // Recipe title title: true, // Recipe title
tags: true, // Recipe tags tags: true, // Recipe tags
loraName: true, // LoRA file name loraName: true, // LoRA file name
loraModel: true // LoRA model name loraModel: true, // LoRA model name
recursive: true
}; };
} }
} }

View File

@@ -96,12 +96,14 @@ export const state = {
isLoading: false, isLoading: false,
hasMore: true, hasMore: true,
sortBy: 'date', sortBy: 'date',
activeFolder: getStorageItem('recipes_activeFolder'),
searchManager: null, searchManager: null,
searchOptions: { searchOptions: {
title: true, title: true,
tags: true, tags: true,
loraName: true, loraName: true,
loraModel: true loraModel: true,
recursive: getStorageItem('recipes_recursiveSearch', true),
}, },
filters: { filters: {
baseModel: [], baseModel: [],

View File

@@ -75,7 +75,9 @@
</div> </div>
</div> </div>
</div> </div>
{% include 'components/folder_sidebar.html' %}
<!-- Recipe grid --> <!-- Recipe grid -->
<div class="card-grid" id="recipeGrid"> <div class="card-grid" id="recipeGrid">
<!-- Remove the server-side conditional rendering and placeholder --> <!-- Remove the server-side conditional rendering and placeholder -->