Enhance Lora and recipe integration with improved filtering and UI updates

- Added support for filtering LoRAs by hash in both API and UI components.
- Implemented session storage management for custom filter states when navigating between recipes and LoRAs.
- Introduced a new button in the recipe modal to view associated LoRAs, enhancing user navigation.
- Updated CSS styles for new UI elements, including a custom filter indicator and LoRA view button.
- Refactored existing JavaScript components to streamline the handling of filter parameters and improve maintainability.
This commit is contained in:
Will Miao
2025-04-08 12:23:51 +08:00
parent bddc7a438d
commit 801aa2e876
12 changed files with 352 additions and 158 deletions

View File

@@ -400,6 +400,27 @@
gap: var(--space-1);
}
/* View LoRAs button */
.view-loras-btn {
background: none;
border: none;
color: var(--text-color);
opacity: 0.7;
cursor: pointer;
padding: 4px 8px;
border-radius: var(--border-radius-xs);
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.view-loras-btn:hover {
opacity: 1;
background: var(--lora-surface);
color: var(--lora-accent);
}
#recipeLorasCount {
font-size: 0.9em;
color: var(--text-color);
@@ -433,6 +454,14 @@
will-change: transform;
/* Create a new containing block for absolutely positioned descendants */
transform: translateZ(0);
cursor: pointer; /* Make it clear the item is clickable */
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}
.recipe-lora-item:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
border-color: var(--lora-accent);
}
.recipe-lora-item.exists-locally {

View File

@@ -4,6 +4,7 @@ import { createLoraCard } from '../components/LoraCard.js';
import { initializeInfiniteScroll } from '../utils/infiniteScroll.js';
import { showDeleteModal } from '../utils/modalUtils.js';
import { toggleFolder } from '../utils/uiHelpers.js';
import { getSessionItem } from '../utils/storageHelpers.js';
export async function loadMoreLoras(resetPage = false, updateFolders = false) {
const pageState = getCurrentPageState();
@@ -57,6 +58,28 @@ export async function loadMoreLoras(resetPage = false, updateFolders = false) {
}
}
// Check for recipe-based filtering parameters from session storage
const filterLoraHash = getSessionItem('recipe_to_lora_filterLoraHash');
const filterLoraHashes = getSessionItem('recipe_to_lora_filterLoraHashes');
console.log('Filter Lora Hash:', filterLoraHash);
console.log('Filter Lora Hashes:', filterLoraHashes);
// Add hash filter parameter if present
if (filterLoraHash) {
params.append('lora_hash', filterLoraHash);
}
// Add multiple hashes filter if present
else if (filterLoraHashes) {
try {
if (Array.isArray(filterLoraHashes) && filterLoraHashes.length > 0) {
params.append('lora_hashes', filterLoraHashes.join(','));
}
} catch (error) {
console.error('Error parsing lora hashes from session storage:', error);
}
}
const response = await fetch(`/api/loras?${params}`);
if (!response.ok) {
throw new Error(`Failed to fetch loras: ${response.statusText}`);

View File

@@ -1,6 +1,7 @@
// Recipe Modal Component
import { showToast } from '../utils/uiHelpers.js';
import { state } from '../state/index.js';
import { setSessionItem, removeSessionItem } from '../utils/storageHelpers.js';
class RecipeModal {
constructor() {
@@ -294,7 +295,7 @@ class RecipeModal {
} else {
// No generation parameters available
if (promptElement) promptElement.textContent = 'No prompt information available';
if (negativePromptElement) negativePromptElement.textContent = 'No negative prompt information available';
if (negativePromptElement) promptElement.textContent = 'No negative prompt information available';
if (otherParamsElement) otherParamsElement.innerHTML = '<div class="no-params">No parameters available</div>';
}
@@ -342,8 +343,15 @@ class RecipeModal {
lorasCountElement.innerHTML = `<i class="fas fa-layer-group"></i> ${totalCount} LoRAs ${statusHTML}`;
// Add click handler for missing LoRAs status
// Add event listeners for buttons and status indicators
setTimeout(() => {
// Set up click handler for View LoRAs button
const viewRecipeLorasBtn = document.getElementById('viewRecipeLorasBtn');
if (viewRecipeLorasBtn) {
viewRecipeLorasBtn.addEventListener('click', () => this.navigateToLorasPage());
}
// Add click handler for missing LoRAs status
const missingStatus = document.querySelector('.recipe-status.missing');
if (missingStatus && missingLorasCount > 0) {
missingStatus.classList.add('clickable');
@@ -433,6 +441,7 @@ class RecipeModal {
// Add event listeners for reconnect functionality
setTimeout(() => {
this.setupReconnectButtons();
this.setupLoraItemsClickable();
}, 100);
// Generate recipe syntax for copy button (this is now a placeholder, actual syntax will be fetched from the API)
@@ -1007,6 +1016,65 @@ class RecipeModal {
state.loadingManager.hide();
}
}
// New method to navigate to the LoRAs page
navigateToLorasPage(specificLoraIndex = null) {
// Close the current modal
modalManager.closeModal('recipeModal');
// Clear any previous filters first
removeSessionItem('recipe_to_lora_filterLoraHash');
removeSessionItem('recipe_to_lora_filterLoraHashes');
removeSessionItem('filterRecipeName');
removeSessionItem('viewLoraDetail');
if (specificLoraIndex !== null) {
// If a specific LoRA index is provided, navigate to view just that one LoRA
const lora = this.currentRecipe.loras[specificLoraIndex];
if (lora && lora.hash) {
// Set session storage to open the LoRA modal directly
setSessionItem('recipe_to_lora_filterLoraHash', lora.hash.toLowerCase());
setSessionItem('viewLoraDetail', 'true');
setSessionItem('filterRecipeName', this.currentRecipe.title);
}
} else {
// If no specific LoRA index is provided, show all LoRAs from this recipe
// Collect all hashes from the recipe's LoRAs
const loraHashes = this.currentRecipe.loras
.filter(lora => lora.hash)
.map(lora => lora.hash.toLowerCase());
if (loraHashes.length > 0) {
// Store the LoRA hashes and recipe name in sessionStorage
setSessionItem('recipe_to_lora_filterLoraHashes', JSON.stringify(loraHashes));
setSessionItem('filterRecipeName', this.currentRecipe.title);
}
}
// Navigate to the LoRAs page
window.location.href = '/loras';
}
// New method to make LoRA items clickable
setupLoraItemsClickable() {
const loraItems = document.querySelectorAll('.recipe-lora-item');
loraItems.forEach(item => {
// Get the lora index from the data attribute
const loraIndex = parseInt(item.dataset.loraIndex);
item.addEventListener('click', (e) => {
// If the click is on the reconnect container or badge, don't navigate
if (e.target.closest('.lora-reconnect-container') ||
e.target.closest('.deleted-badge') ||
e.target.closest('.reconnect-tooltip')) {
return;
}
// Navigate to the LoRAs page with the specific LoRA index
this.navigateToLorasPage(loraIndex);
});
});
}
}
export { RecipeModal };
export { RecipeModal };

View File

@@ -192,29 +192,20 @@ function copyRecipeSyntax(recipeId) {
* @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) {
function navigateToRecipesPage(loraName, loraHash) {
// Close the current modal
if (window.modalManager) {
modalManager.closeModal('loraModal');
}
// Clear any previous filters first
removeSessionItem('filterLoraName');
removeSessionItem('filterLoraHash');
removeSessionItem('bypassExistingFilters');
removeSessionItem('lora_to_recipe_filterLoraName');
removeSessionItem('lora_to_recipe_filterLoraHash');
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');
}
setSessionItem('lora_to_recipe_filterLoraName', loraName);
setSessionItem('lora_to_recipe_filterLoraHash', loraHash);
// Directly navigate to recipes page
window.location.href = '/loras/recipes';
@@ -233,7 +224,6 @@ function navigateToRecipeDetails(recipeId) {
// Clear any previous filters first
removeSessionItem('filterLoraName');
removeSessionItem('filterLoraHash');
removeSessionItem('bypassExistingFilters');
removeSessionItem('viewRecipeId');
// Store the recipe ID in sessionStorage to load on recipes page

View File

@@ -17,7 +17,7 @@ import { LoraContextMenu } from './components/ContextMenu.js';
import { moveManager } from './managers/MoveManager.js';
import { updateCardsForBulkMode } from './components/LoraCard.js';
import { bulkManager } from './managers/BulkManager.js';
import { setStorageItem, getStorageItem } from './utils/storageHelpers.js';
import { setStorageItem, getStorageItem, getSessionItem, removeSessionItem } from './utils/storageHelpers.js';
// Initialize the LoRA page
class LoraPageManager {
@@ -69,6 +69,9 @@ class LoraPageManager {
initFolderTagsVisibility();
new LoraContextMenu();
// Check for custom filters from recipe page navigation
this.checkCustomFilters();
// Initialize cards for current bulk mode state (should be false initially)
updateCardsForBulkMode(state.bulkMode);
@@ -79,6 +82,80 @@ class LoraPageManager {
appCore.initializePageFeatures();
}
// Check for custom filter parameters in session storage
checkCustomFilters() {
const filterLoraHash = getSessionItem('recipe_to_lora_filterLoraHash');
const filterLoraHashes = getSessionItem('recipe_to_lora_filterLoraHashes');
const filterRecipeName = getSessionItem('filterRecipeName');
const viewLoraDetail = getSessionItem('viewLoraDetail');
console.log("Checking custom filters...");
console.log("filterLoraHash:", filterLoraHash);
console.log("filterLoraHashes:", filterLoraHashes);
console.log("filterRecipeName:", filterRecipeName);
console.log("viewLoraDetail:", viewLoraDetail);
if ((filterLoraHash || filterLoraHashes) && filterRecipeName) {
// Found custom filter parameters, set up the custom filter
// Show the filter indicator
const indicator = document.getElementById('customFilterIndicator');
const filterText = indicator.querySelector('.customFilterText');
if (indicator && filterText) {
indicator.classList.remove('hidden');
// Set text content with recipe name
const filterType = filterLoraHash && viewLoraDetail ? "Viewing LoRA from" : "Viewing LoRAs from";
const displayText = `${filterType}: ${filterRecipeName}`;
filterText.textContent = this._truncateText(displayText, 30);
filterText.setAttribute('title', displayText);
// Add click handler for the clear button
const clearBtn = indicator.querySelector('.clear-filter');
if (clearBtn) {
clearBtn.addEventListener('click', this.clearCustomFilter);
}
}
// If we're viewing a specific LoRA detail, set up to open the modal
if (filterLoraHash && viewLoraDetail) {
// Store this to fetch after initial load completes
state.pendingLoraHash = filterLoraHash;
}
}
}
// Helper to truncate text with ellipsis
_truncateText(text, maxLength) {
return text.length > maxLength ? text.substring(0, maxLength - 3) + '...' : text;
}
// Clear the custom filter and reload the page
clearCustomFilter = async () => {
console.log("Clearing custom filter...");
// Remove filter parameters from session storage
removeSessionItem('recipe_to_lora_filterLoraHash');
removeSessionItem('recipe_to_lora_filterLoraHashes');
removeSessionItem('filterRecipeName');
removeSessionItem('viewLoraDetail');
// Hide the filter indicator
const indicator = document.getElementById('customFilterIndicator');
if (indicator) {
indicator.classList.add('hidden');
}
// Reset state
if (state.pendingLoraHash) {
delete state.pendingLoraHash;
}
// Reload the loras
await resetAndReload();
}
loadSortPreference() {
const savedSort = getStorageItem('loras_sort');
if (savedSort) {

View File

@@ -71,18 +71,15 @@ class RecipeManager {
}
_checkCustomFilter() {
// Check for bypass filter flag
const bypassExistingFilters = getSessionItem('bypassExistingFilters');
// Check for Lora filter
const filterLoraName = getSessionItem('filterLoraName');
const filterLoraHash = getSessionItem('filterLoraHash');
const filterLoraName = getSessionItem('lora_to_recipe_filterLoraName');
const filterLoraHash = getSessionItem('lora_to_recipe_filterLoraHash');
// Check for specific recipe ID
const viewRecipeId = getSessionItem('viewRecipeId');
// Set custom filter if any parameter is present
if (bypassExistingFilters || filterLoraName || filterLoraHash || viewRecipeId) {
if (filterLoraName || filterLoraHash || viewRecipeId) {
this.customFilter = {
active: true,
loraName: filterLoraName,
@@ -90,26 +87,9 @@ class RecipeManager {
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() {
@@ -176,8 +156,8 @@ class RecipeManager {
}
// Clear any session storage items
removeSessionItem('filterLoraName');
removeSessionItem('filterLoraHash');
removeSessionItem('lora_to_recipe_filterLoraName');
removeSessionItem('lora_to_recipe_filterLoraHash');
removeSessionItem('viewRecipeId');
// Reload recipes without custom filter
@@ -366,8 +346,4 @@ document.addEventListener('DOMContentLoaded', async () => {
});
// Export for use in other modules
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...
export { RecipeManager };