mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-23 22:22:11 -03:00
feat: Add Lora recipes retrieval and filtering functionality
- Implemented a new API endpoint to fetch recipes associated with a specific Lora by its hash. - Enhanced the recipe scanning logic to support filtering by Lora hash and bypassing other filters. - Added a new method to retrieve a recipe by its ID with formatted metadata. - Created a new RecipeTab component to display recipes in the Lora modal. - Introduced session storage utilities for managing custom filter states. - Updated the UI to include a custom filter indicator and loading/error states for recipes. - Refactored existing recipe management logic to accommodate new features and improve maintainability.
This commit is contained in:
65
static/css/components/filter-indicator.css
Normal file
65
static/css/components/filter-indicator.css
Normal file
@@ -0,0 +1,65 @@
|
||||
/* Filter indicator styles */
|
||||
.control-group .filter-active {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: var(--lora-accent);
|
||||
color: white;
|
||||
border-radius: 20px;
|
||||
padding: 6px 12px;
|
||||
transition: all 0.3s ease;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.control-group .filter-active:hover {
|
||||
background: var(--lora-accent);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.control-group .filter-active i.fa-filter {
|
||||
font-size: 0.9em;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.control-group .filter-active i.clear-filter {
|
||||
transition: transform 0.2s ease;
|
||||
cursor: pointer;
|
||||
margin-left: 4px;
|
||||
border-radius: 50%;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.control-group .filter-active i.clear-filter:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.control-group .filter-active .lora-name {
|
||||
font-weight: 500;
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Animation for filter indicator */
|
||||
@keyframes filterPulse {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
.filter-active.animate {
|
||||
animation: filterPulse 0.6s ease;
|
||||
}
|
||||
|
||||
/* Make responsive */
|
||||
@media (max-width: 576px) {
|
||||
.control-group .filter-active {
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.control-group .filter-active .lora-name {
|
||||
max-width: 100px;
|
||||
}
|
||||
}
|
||||
@@ -863,7 +863,7 @@
|
||||
}
|
||||
|
||||
.model-description-content blockquote {
|
||||
border-left: 3px solid var(--lora-accent);
|
||||
border-left: 3px solid var (--lora-accent);
|
||||
padding-left: 1em;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
@@ -1280,4 +1280,47 @@
|
||||
font-size: 1.1em;
|
||||
color: var(--lora-accent);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.view-all-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 6px 12px;
|
||||
background-color: var(--lora-accent);
|
||||
color: var(--lora-text);
|
||||
border: none;
|
||||
border-radius: var(--border-radius-sm);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.view-all-btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Loading, error and empty states */
|
||||
.recipes-loading,
|
||||
.recipes-error,
|
||||
.recipes-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.recipes-loading i,
|
||||
.recipes-error i,
|
||||
.recipes-empty i {
|
||||
font-size: 32px;
|
||||
margin-bottom: 15px;
|
||||
color: var(--lora-accent);
|
||||
}
|
||||
|
||||
.recipes-error i {
|
||||
color: var(--lora-error);
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
@import 'components/search-filter.css';
|
||||
@import 'components/bulk.css';
|
||||
@import 'components/shared.css';
|
||||
@import 'components/filter-indicator.css';
|
||||
|
||||
.initialization-notice {
|
||||
display: flex;
|
||||
|
||||
244
static/js/components/loraModal/RecipeTab.js
Normal file
244
static/js/components/loraModal/RecipeTab.js
Normal file
@@ -0,0 +1,244 @@
|
||||
/**
|
||||
* RecipeTab - Handles the recipes tab in the Lora Modal
|
||||
*/
|
||||
import { showToast } from '../../utils/uiHelpers.js';
|
||||
import { setSessionItem, removeSessionItem } from '../../utils/storageHelpers.js';
|
||||
|
||||
/**
|
||||
* Loads recipes that use the specified Lora and renders them in the tab
|
||||
* @param {string} loraName - The display name of the Lora
|
||||
* @param {string} sha256 - The SHA256 hash of the Lora
|
||||
*/
|
||||
export function loadRecipesForLora(loraName, sha256) {
|
||||
const recipeTab = document.getElementById('recipes-tab');
|
||||
if (!recipeTab) return;
|
||||
|
||||
// Show loading state
|
||||
recipeTab.innerHTML = `
|
||||
<div class="recipes-loading">
|
||||
<i class="fas fa-spinner fa-spin"></i> Loading recipes...
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Fetch recipes that use this Lora by hash
|
||||
fetch(`/api/recipes/for-lora?hash=${encodeURIComponent(sha256.toLowerCase())}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (!data.success) {
|
||||
throw new Error(data.error || 'Failed to load recipes');
|
||||
}
|
||||
|
||||
renderRecipes(recipeTab, data.recipes, loraName, sha256);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading recipes for Lora:', error);
|
||||
recipeTab.innerHTML = `
|
||||
<div class="recipes-error">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
<p>Failed to load recipes. Please try again later.</p>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the recipe cards in the tab
|
||||
* @param {HTMLElement} tabElement - The tab element to render into
|
||||
* @param {Array} recipes - Array of recipe objects
|
||||
* @param {string} loraName - The display name of the Lora
|
||||
* @param {string} loraHash - The hash of the Lora
|
||||
*/
|
||||
function renderRecipes(tabElement, recipes, loraName, loraHash) {
|
||||
if (!recipes || recipes.length === 0) {
|
||||
tabElement.innerHTML = `
|
||||
<div class="recipes-empty">
|
||||
<i class="fas fa-book-open"></i>
|
||||
<p>No recipes found that use this Lora.</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Create header with count and view all button
|
||||
const headerElement = document.createElement('div');
|
||||
headerElement.className = 'recipes-header';
|
||||
headerElement.innerHTML = `
|
||||
<h3>Found ${recipes.length} recipe${recipes.length > 1 ? 's' : ''} using this Lora</h3>
|
||||
<button class="view-all-btn" title="View all in Recipes page">
|
||||
<i class="fas fa-external-link-alt"></i> View All in Recipes
|
||||
</button>
|
||||
`;
|
||||
|
||||
// Add click handler for "View All" button
|
||||
headerElement.querySelector('.view-all-btn').addEventListener('click', () => {
|
||||
navigateToRecipesPage(loraName, loraHash);
|
||||
});
|
||||
|
||||
// Create grid container for recipe cards
|
||||
const cardGrid = document.createElement('div');
|
||||
cardGrid.className = 'card-grid';
|
||||
|
||||
// Create recipe cards matching the structure in recipes.html
|
||||
recipes.forEach(recipe => {
|
||||
// Get basic info
|
||||
const baseModel = recipe.base_model || '';
|
||||
const loras = recipe.loras || [];
|
||||
const lorasCount = loras.length;
|
||||
const missingLorasCount = loras.filter(lora => !lora.inLibrary && !lora.isDeleted).length;
|
||||
const allLorasAvailable = missingLorasCount === 0 && lorasCount > 0;
|
||||
|
||||
// Ensure file_url exists, fallback to file_path if needed
|
||||
const imageUrl = recipe.file_url ||
|
||||
(recipe.file_path ? `/loras_static/root1/preview/${recipe.file_path.split('/').pop()}` :
|
||||
'/loras_static/images/no-preview.png');
|
||||
|
||||
// Create card element matching the structure in recipes.html
|
||||
const card = document.createElement('div');
|
||||
card.className = 'lora-card';
|
||||
card.dataset.filePath = recipe.file_path || '';
|
||||
card.dataset.title = recipe.title || '';
|
||||
card.dataset.created = recipe.created_date || '';
|
||||
card.dataset.id = recipe.id || '';
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="recipe-indicator" title="Recipe">R</div>
|
||||
<div class="card-preview">
|
||||
<img src="${imageUrl}" alt="${recipe.title}" loading="lazy">
|
||||
<div class="card-header">
|
||||
<div class="base-model-wrapper">
|
||||
${baseModel ? `<span class="base-model-label" title="${baseModel}">${baseModel}</span>` : ''}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<i class="fas fa-copy" title="Copy Recipe Syntax"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="model-info">
|
||||
<span class="model-name">${recipe.title}</span>
|
||||
</div>
|
||||
<div class="lora-count ${allLorasAvailable ? 'ready' : (lorasCount > 0 ? 'missing' : '')}"
|
||||
title="${getLoraStatusTitle(lorasCount, missingLorasCount)}">
|
||||
<i class="fas fa-layer-group"></i> ${lorasCount}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add event listeners for action buttons
|
||||
card.querySelector('.fa-copy').addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
copyRecipeSyntax(recipe.id);
|
||||
});
|
||||
|
||||
// Add click handler for the entire card
|
||||
card.addEventListener('click', () => {
|
||||
navigateToRecipeDetails(recipe.id);
|
||||
});
|
||||
|
||||
// Add card to grid
|
||||
cardGrid.appendChild(card);
|
||||
});
|
||||
|
||||
// Clear loading indicator and append content
|
||||
tabElement.innerHTML = '';
|
||||
tabElement.appendChild(headerElement);
|
||||
tabElement.appendChild(cardGrid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a descriptive title for the LoRA status indicator
|
||||
* @param {number} totalCount - Total number of LoRAs in recipe
|
||||
* @param {number} missingCount - Number of missing LoRAs
|
||||
* @returns {string} Status title text
|
||||
*/
|
||||
function getLoraStatusTitle(totalCount, missingCount) {
|
||||
if (totalCount === 0) return "No LoRAs in this recipe";
|
||||
if (missingCount === 0) return "All LoRAs available - Ready to use";
|
||||
return `${missingCount} of ${totalCount} LoRAs missing`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies recipe syntax to clipboard
|
||||
* @param {string} recipeId - The recipe ID
|
||||
*/
|
||||
function copyRecipeSyntax(recipeId) {
|
||||
if (!recipeId) {
|
||||
showToast('Cannot copy recipe syntax: Missing recipe ID', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/api/recipe/${recipeId}/syntax`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success && data.syntax) {
|
||||
return navigator.clipboard.writeText(data.syntax);
|
||||
} else {
|
||||
throw new Error(data.error || 'No syntax returned');
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
showToast('Recipe syntax copied to clipboard', 'success');
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to copy: ', err);
|
||||
showToast('Failed to copy recipe syntax', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the recipes page with filter for the current Lora
|
||||
* @param {string} loraName - The Lora display name to filter by
|
||||
* @param {string} loraHash - The hash of the Lora to filter by
|
||||
* @param {boolean} createNew - Whether to open the create recipe dialog
|
||||
*/
|
||||
function navigateToRecipesPage(loraName, loraHash, createNew = false) {
|
||||
// Close the current modal
|
||||
if (window.modalManager) {
|
||||
modalManager.closeModal('loraModal');
|
||||
}
|
||||
|
||||
// Clear any previous filters first
|
||||
removeSessionItem('filterLoraName');
|
||||
removeSessionItem('filterLoraHash');
|
||||
removeSessionItem('bypassExistingFilters');
|
||||
removeSessionItem('viewRecipeId');
|
||||
|
||||
// Store the LoRA name and hash filter in sessionStorage
|
||||
setSessionItem('filterLoraName', loraName);
|
||||
setSessionItem('filterLoraHash', loraHash);
|
||||
|
||||
// Set a flag to indicate we're navigating with a specific filter
|
||||
setSessionItem('bypassExistingFilters', 'true');
|
||||
|
||||
// Set flag to open create dialog if requested
|
||||
if (createNew) {
|
||||
setSessionItem('openCreateRecipeDialog', 'true');
|
||||
}
|
||||
|
||||
// Directly navigate to recipes page
|
||||
window.location.href = '/loras/recipes';
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates directly to a specific recipe's details
|
||||
* @param {string} recipeId - The recipe ID to view
|
||||
*/
|
||||
function navigateToRecipeDetails(recipeId) {
|
||||
// Close the current modal
|
||||
if (window.modalManager) {
|
||||
modalManager.closeModal('loraModal');
|
||||
}
|
||||
|
||||
// Clear any previous filters first
|
||||
removeSessionItem('filterLoraName');
|
||||
removeSessionItem('filterLoraHash');
|
||||
removeSessionItem('bypassExistingFilters');
|
||||
removeSessionItem('viewRecipeId');
|
||||
|
||||
// Store the recipe ID in sessionStorage to load on recipes page
|
||||
setSessionItem('viewRecipeId', recipeId);
|
||||
|
||||
// Directly navigate to recipes page
|
||||
window.location.href = '/loras/recipes';
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { renderShowcaseContent, toggleShowcase, setupShowcaseScroll, scrollToTop
|
||||
import { setupTabSwitching, loadModelDescription } from './ModelDescription.js';
|
||||
import { renderTriggerWords, setupTriggerWordsEditMode } from './TriggerWords.js';
|
||||
import { parsePresets, renderPresetTags } from './PresetTags.js';
|
||||
import { loadRecipesForLora } from './RecipeTab.js'; // Add import for recipe tab
|
||||
import {
|
||||
setupModelNameEditing,
|
||||
setupBaseModelEditing,
|
||||
@@ -116,6 +117,7 @@ export function showLoraModal(lora) {
|
||||
<div class="showcase-tabs">
|
||||
<button class="tab-btn active" data-tab="showcase">Examples</button>
|
||||
<button class="tab-btn" data-tab="description">Model Description</button>
|
||||
<button class="tab-btn" data-tab="recipes">Recipes</button>
|
||||
</div>
|
||||
|
||||
<div class="tab-content">
|
||||
@@ -133,6 +135,12 @@ export function showLoraModal(lora) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="recipes-tab" class="tab-pane">
|
||||
<div class="recipes-loading">
|
||||
<i class="fas fa-spinner fa-spin"></i> Loading recipes...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="back-to-top" onclick="scrollToTop(this)">
|
||||
@@ -157,6 +165,9 @@ export function showLoraModal(lora) {
|
||||
if (lora.civitai?.modelId && !lora.modelDescription) {
|
||||
loadModelDescription(lora.civitai.modelId, lora.file_path);
|
||||
}
|
||||
|
||||
// Load recipes for this Lora
|
||||
loadRecipesForLora(lora.model_name, lora.sha256);
|
||||
}
|
||||
|
||||
// Copy file name function
|
||||
|
||||
@@ -5,6 +5,7 @@ import { RecipeCard } from './components/RecipeCard.js';
|
||||
import { RecipeModal } from './components/RecipeModal.js';
|
||||
import { getCurrentPageState } from './state/index.js';
|
||||
import { toggleApiKeyVisibility } from './managers/SettingsManager.js';
|
||||
import { getSessionItem, removeSessionItem } from './utils/storageHelpers.js';
|
||||
|
||||
class RecipeManager {
|
||||
constructor() {
|
||||
@@ -20,6 +21,14 @@ class RecipeManager {
|
||||
// Add state tracking for infinite scroll
|
||||
this.pageState.isLoading = false;
|
||||
this.pageState.hasMore = true;
|
||||
|
||||
// Custom filter state
|
||||
this.customFilter = {
|
||||
active: false,
|
||||
loraName: null,
|
||||
loraHash: null,
|
||||
recipeId: null
|
||||
};
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
@@ -29,6 +38,9 @@ class RecipeManager {
|
||||
// Set default search options if not already defined
|
||||
this._initSearchOptions();
|
||||
|
||||
// Check for custom filter parameters in session storage
|
||||
this._checkCustomFilter();
|
||||
|
||||
// Load initial set of recipes
|
||||
await this.loadRecipes();
|
||||
|
||||
@@ -58,6 +70,120 @@ class RecipeManager {
|
||||
window.toggleApiKeyVisibility = toggleApiKeyVisibility;
|
||||
}
|
||||
|
||||
_checkCustomFilter() {
|
||||
// Check for bypass filter flag
|
||||
const bypassExistingFilters = getSessionItem('bypassExistingFilters');
|
||||
|
||||
// Check for Lora filter
|
||||
const filterLoraName = getSessionItem('filterLoraName');
|
||||
const filterLoraHash = getSessionItem('filterLoraHash');
|
||||
|
||||
// Check for specific recipe ID
|
||||
const viewRecipeId = getSessionItem('viewRecipeId');
|
||||
|
||||
// Set custom filter if any parameter is present
|
||||
if (bypassExistingFilters || filterLoraName || filterLoraHash || viewRecipeId) {
|
||||
this.customFilter = {
|
||||
active: true,
|
||||
loraName: filterLoraName,
|
||||
loraHash: filterLoraHash,
|
||||
recipeId: viewRecipeId
|
||||
};
|
||||
|
||||
// Clean up session storage after reading
|
||||
removeSessionItem('bypassExistingFilters');
|
||||
|
||||
// Show custom filter indicator
|
||||
this._showCustomFilterIndicator();
|
||||
}
|
||||
|
||||
// Check for create recipe dialog flag
|
||||
const openCreateRecipeDialog = getSessionItem('openCreateRecipeDialog');
|
||||
if (openCreateRecipeDialog) {
|
||||
// Clean up session storage
|
||||
removeSessionItem('openCreateRecipeDialog');
|
||||
|
||||
// Schedule showing the create dialog after the page loads
|
||||
setTimeout(() => {
|
||||
if (this.importManager && typeof this.importManager.showImportModal === 'function') {
|
||||
this.importManager.showImportModal();
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
_showCustomFilterIndicator() {
|
||||
const indicator = document.getElementById('customFilterIndicator');
|
||||
const textElement = document.getElementById('customFilterText');
|
||||
|
||||
if (!indicator || !textElement) return;
|
||||
|
||||
// Update text based on filter type
|
||||
let filterText = '';
|
||||
|
||||
if (this.customFilter.recipeId) {
|
||||
filterText = 'Viewing specific recipe';
|
||||
} else if (this.customFilter.loraName) {
|
||||
// Format with Lora name
|
||||
const loraName = this.customFilter.loraName;
|
||||
const displayName = loraName.length > 25 ?
|
||||
loraName.substring(0, 22) + '...' :
|
||||
loraName;
|
||||
|
||||
filterText = `<span>Recipes using: <span class="lora-name">${displayName}</span></span>`;
|
||||
} else {
|
||||
filterText = 'Filtered recipes';
|
||||
}
|
||||
|
||||
// Update indicator text and show it
|
||||
textElement.innerHTML = filterText;
|
||||
// Add title attribute to show the lora name as a tooltip
|
||||
if (this.customFilter.loraName) {
|
||||
textElement.setAttribute('title', this.customFilter.loraName);
|
||||
}
|
||||
indicator.classList.remove('hidden');
|
||||
|
||||
// Add pulse animation
|
||||
const button = indicator.querySelector('button');
|
||||
if (button) {
|
||||
button.classList.add('animate');
|
||||
setTimeout(() => button.classList.remove('animate'), 600);
|
||||
}
|
||||
|
||||
// Add click handler for clear filter button
|
||||
const clearFilterBtn = indicator.querySelector('.clear-filter');
|
||||
if (clearFilterBtn) {
|
||||
clearFilterBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation(); // Prevent button click from triggering
|
||||
this._clearCustomFilter();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_clearCustomFilter() {
|
||||
// Reset custom filter
|
||||
this.customFilter = {
|
||||
active: false,
|
||||
loraName: null,
|
||||
loraHash: null,
|
||||
recipeId: null
|
||||
};
|
||||
|
||||
// Hide indicator
|
||||
const indicator = document.getElementById('customFilterIndicator');
|
||||
if (indicator) {
|
||||
indicator.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Clear any session storage items
|
||||
removeSessionItem('filterLoraName');
|
||||
removeSessionItem('filterLoraHash');
|
||||
removeSessionItem('viewRecipeId');
|
||||
|
||||
// Reload recipes without custom filter
|
||||
this.loadRecipes();
|
||||
}
|
||||
|
||||
initEventListeners() {
|
||||
// Sort select
|
||||
const sortSelect = document.getElementById('sortSelect');
|
||||
@@ -83,6 +209,12 @@ class RecipeManager {
|
||||
if (grid) grid.innerHTML = '';
|
||||
}
|
||||
|
||||
// If we have a specific recipe ID to load
|
||||
if (this.customFilter.active && this.customFilter.recipeId) {
|
||||
await this._loadSpecificRecipe(this.customFilter.recipeId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Build query parameters
|
||||
const params = new URLSearchParams({
|
||||
page: this.pageState.currentPage,
|
||||
@@ -90,28 +222,38 @@ class RecipeManager {
|
||||
sort_by: this.pageState.sortBy
|
||||
});
|
||||
|
||||
// Add search filter if present
|
||||
if (this.pageState.filters.search) {
|
||||
params.append('search', this.pageState.filters.search);
|
||||
// Add custom filter for Lora if present
|
||||
if (this.customFilter.active && this.customFilter.loraHash) {
|
||||
params.append('lora_hash', this.customFilter.loraHash);
|
||||
|
||||
// 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');
|
||||
// Skip other filters when using custom filter
|
||||
params.append('bypass_filters', 'true');
|
||||
} else {
|
||||
// Normal filtering logic
|
||||
|
||||
// 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
|
||||
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(','));
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -139,6 +281,46 @@ class RecipeManager {
|
||||
}
|
||||
}
|
||||
|
||||
async _loadSpecificRecipe(recipeId) {
|
||||
try {
|
||||
// Fetch specific recipe by ID
|
||||
const response = await fetch(`/api/recipe/${recipeId}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load recipe: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const recipe = await response.json();
|
||||
|
||||
// Create a data structure that matches the expected format
|
||||
const recipeData = {
|
||||
items: [recipe],
|
||||
total: 1,
|
||||
page: 1,
|
||||
page_size: 1,
|
||||
total_pages: 1
|
||||
};
|
||||
|
||||
// Update grid with single recipe
|
||||
this.updateRecipesGrid(recipeData, true);
|
||||
|
||||
// Pagination not needed for single recipe
|
||||
this.pageState.hasMore = false;
|
||||
|
||||
// Show recipe details modal
|
||||
setTimeout(() => {
|
||||
this.showRecipeDetails(recipe);
|
||||
}, 300);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading specific recipe:', error);
|
||||
appCore.showToast('Failed to load recipe details', 'error');
|
||||
|
||||
// Clear the filter and show all recipes
|
||||
this._clearCustomFilter();
|
||||
}
|
||||
}
|
||||
|
||||
updateRecipesGrid(data, resetGrid = true) {
|
||||
const grid = document.getElementById('recipeGrid');
|
||||
if (!grid) return;
|
||||
@@ -184,4 +366,8 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
});
|
||||
|
||||
// Export for use in other modules
|
||||
export { RecipeManager };
|
||||
export { RecipeManager };
|
||||
|
||||
// The RecipesManager class from the original file is preserved below (commented out)
|
||||
// If needed, functionality can be migrated to the new RecipeManager class above
|
||||
// ...rest of the existing code...
|
||||
@@ -69,6 +69,53 @@ export function removeStorageItem(key) {
|
||||
localStorage.removeItem(key); // Also remove legacy key
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an item from sessionStorage with namespace support
|
||||
* @param {string} key - The key without prefix
|
||||
* @param {any} defaultValue - Default value if key doesn't exist
|
||||
* @returns {any} The stored value or defaultValue
|
||||
*/
|
||||
export function getSessionItem(key, defaultValue = null) {
|
||||
// Try with prefix
|
||||
const prefixedValue = sessionStorage.getItem(STORAGE_PREFIX + key);
|
||||
|
||||
if (prefixedValue !== null) {
|
||||
// If it's a JSON string, parse it
|
||||
try {
|
||||
return JSON.parse(prefixedValue);
|
||||
} catch (e) {
|
||||
return prefixedValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Return default value if key doesn't exist
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an item in sessionStorage with namespace prefix
|
||||
* @param {string} key - The key without prefix
|
||||
* @param {any} value - The value to store
|
||||
*/
|
||||
export function setSessionItem(key, value) {
|
||||
const prefixedKey = STORAGE_PREFIX + key;
|
||||
|
||||
// Convert objects and arrays to JSON strings
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
sessionStorage.setItem(prefixedKey, JSON.stringify(value));
|
||||
} else {
|
||||
sessionStorage.setItem(prefixedKey, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an item from sessionStorage with namespace prefix
|
||||
* @param {string} key - The key without prefix
|
||||
*/
|
||||
export function removeSessionItem(key) {
|
||||
sessionStorage.removeItem(STORAGE_PREFIX + key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate all existing localStorage items to use the prefix
|
||||
* This should be called once during application initialization
|
||||
|
||||
Reference in New Issue
Block a user