mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-26 07:35:44 -03:00
Enhance LoRA and Recipe templates by adding request context to template rendering. Update JavaScript to initialize search managers with context-specific options and improve header navigation with dynamic search placeholders. Refactor header component for better context awareness in search functionality.
This commit is contained in:
@@ -71,7 +71,8 @@ class LoraRoutes:
|
|||||||
rendered = template.render(
|
rendered = template.render(
|
||||||
folders=[], # 空文件夹列表
|
folders=[], # 空文件夹列表
|
||||||
is_initializing=True, # 新增标志
|
is_initializing=True, # 新增标志
|
||||||
settings=settings # Pass settings to template
|
settings=settings, # Pass settings to template
|
||||||
|
request=request # Pass the request object to the template
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# 正常流程
|
# 正常流程
|
||||||
@@ -80,7 +81,8 @@ class LoraRoutes:
|
|||||||
rendered = template.render(
|
rendered = template.render(
|
||||||
folders=cache.folders,
|
folders=cache.folders,
|
||||||
is_initializing=False,
|
is_initializing=False,
|
||||||
settings=settings # Pass settings to template
|
settings=settings, # Pass settings to template
|
||||||
|
request=request # Pass the request object to the template
|
||||||
)
|
)
|
||||||
|
|
||||||
return web.Response(
|
return web.Response(
|
||||||
@@ -110,7 +112,8 @@ class LoraRoutes:
|
|||||||
template = self.template_env.get_template('recipes.html')
|
template = self.template_env.get_template('recipes.html')
|
||||||
rendered = template.render(
|
rendered = template.render(
|
||||||
is_initializing=True,
|
is_initializing=True,
|
||||||
settings=settings
|
settings=settings,
|
||||||
|
request=request # Pass the request object to the template
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Normal flow - get recipes with the same formatting as the API endpoint
|
# Normal flow - get recipes with the same formatting as the API endpoint
|
||||||
@@ -137,7 +140,8 @@ class LoraRoutes:
|
|||||||
rendered = template.render(
|
rendered = template.render(
|
||||||
recipes=recipes_data,
|
recipes=recipes_data,
|
||||||
is_initializing=False,
|
is_initializing=False,
|
||||||
settings=settings
|
settings=settings,
|
||||||
|
request=request # Pass the request object to the template
|
||||||
)
|
)
|
||||||
|
|
||||||
return web.Response(
|
return web.Response(
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
} from './utils/uiHelpers.js';
|
} from './utils/uiHelpers.js';
|
||||||
import { initializeInfiniteScroll } from './utils/infiniteScroll.js';
|
import { initializeInfiniteScroll } from './utils/infiniteScroll.js';
|
||||||
import { showDeleteModal, confirmDelete, closeDeleteModal } from './utils/modalUtils.js';
|
import { showDeleteModal, confirmDelete, closeDeleteModal } from './utils/modalUtils.js';
|
||||||
import { SearchManager } from './utils/search.js';
|
import { SearchManager } from './managers/SearchManager.js';
|
||||||
import { DownloadManager } from './managers/DownloadManager.js';
|
import { DownloadManager } from './managers/DownloadManager.js';
|
||||||
import { SettingsManager, toggleApiKeyVisibility } from './managers/SettingsManager.js';
|
import { SettingsManager, toggleApiKeyVisibility } from './managers/SettingsManager.js';
|
||||||
import { LoraContextMenu } from './components/ContextMenu.js';
|
import { LoraContextMenu } from './components/ContextMenu.js';
|
||||||
@@ -104,6 +104,22 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
|
|
||||||
// Update positions on window resize
|
// Update positions on window resize
|
||||||
window.addEventListener('resize', updatePanelPositions);
|
window.addEventListener('resize', updatePanelPositions);
|
||||||
|
|
||||||
|
// Initialize search manager with LoRA-specific options
|
||||||
|
const loraSearchManager = new SearchManager({
|
||||||
|
searchCallback: (query, options, recursive) => {
|
||||||
|
// LoRA-specific search implementation
|
||||||
|
// This could call your API with the right parameters
|
||||||
|
fetchLoras({
|
||||||
|
search: query,
|
||||||
|
search_options: options,
|
||||||
|
recursive: recursive
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set the current page for proper context
|
||||||
|
document.body.dataset.page = 'loras';
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize event listeners
|
// Initialize event listeners
|
||||||
|
|||||||
360
static/js/managers/RecipeFilterManager.js
Normal file
360
static/js/managers/RecipeFilterManager.js
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
import { showToast } from '../utils/uiHelpers.js';
|
||||||
|
|
||||||
|
export class RecipeFilterManager {
|
||||||
|
constructor() {
|
||||||
|
this.filters = {
|
||||||
|
baseModel: [],
|
||||||
|
tags: []
|
||||||
|
};
|
||||||
|
|
||||||
|
this.filterPanel = document.getElementById('filterPanel');
|
||||||
|
this.filterButton = document.getElementById('filterButton');
|
||||||
|
this.activeFiltersCount = document.getElementById('activeFiltersCount');
|
||||||
|
this.tagsLoaded = false;
|
||||||
|
|
||||||
|
this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
// Create base model filter tags if they exist
|
||||||
|
if (document.getElementById('baseModelTags')) {
|
||||||
|
this.createBaseModelTags();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close filter panel when clicking outside
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (!this.filterPanel.contains(e.target) &&
|
||||||
|
e.target !== this.filterButton &&
|
||||||
|
!this.filterButton.contains(e.target) &&
|
||||||
|
!this.filterPanel.classList.contains('hidden')) {
|
||||||
|
this.closeFilterPanel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add click handler for filter button
|
||||||
|
if (this.filterButton) {
|
||||||
|
this.filterButton.addEventListener('click', () => {
|
||||||
|
this.toggleFilterPanel();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize active filters from localStorage if available
|
||||||
|
this.loadFiltersFromStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadTopTags() {
|
||||||
|
try {
|
||||||
|
// Show loading state
|
||||||
|
const tagsContainer = document.getElementById('modelTagsFilter');
|
||||||
|
if (tagsContainer) {
|
||||||
|
tagsContainer.innerHTML = '<div class="tags-loading">Loading tags...</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch('/api/recipes/top-tags?limit=20');
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch recipe tags');
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success && data.tags) {
|
||||||
|
this.createTagFilterElements(data.tags);
|
||||||
|
|
||||||
|
// After creating tag elements, mark any previously selected ones
|
||||||
|
this.updateTagSelections();
|
||||||
|
} else {
|
||||||
|
throw new Error('Invalid response format');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading top recipe tags:', error);
|
||||||
|
const tagsContainer = document.getElementById('modelTagsFilter');
|
||||||
|
if (tagsContainer) {
|
||||||
|
tagsContainer.innerHTML = '<div class="tags-error">Failed to load tags</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createTagFilterElements(tags) {
|
||||||
|
const tagsContainer = document.getElementById('modelTagsFilter');
|
||||||
|
if (!tagsContainer) return;
|
||||||
|
|
||||||
|
tagsContainer.innerHTML = '';
|
||||||
|
|
||||||
|
if (!tags.length) {
|
||||||
|
tagsContainer.innerHTML = '<div class="no-tags">No recipe tags available</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tags.forEach(tag => {
|
||||||
|
const tagEl = document.createElement('div');
|
||||||
|
tagEl.className = 'filter-tag tag-filter';
|
||||||
|
const tagName = tag.tag;
|
||||||
|
tagEl.dataset.tag = tagName;
|
||||||
|
tagEl.innerHTML = `${tagName} <span class="tag-count">${tag.count}</span>`;
|
||||||
|
|
||||||
|
// Add click handler to toggle selection and automatically apply
|
||||||
|
tagEl.addEventListener('click', async () => {
|
||||||
|
tagEl.classList.toggle('active');
|
||||||
|
|
||||||
|
if (tagEl.classList.contains('active')) {
|
||||||
|
if (!this.filters.tags.includes(tagName)) {
|
||||||
|
this.filters.tags.push(tagName);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.filters.tags = this.filters.tags.filter(t => t !== tagName);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateActiveFiltersCount();
|
||||||
|
|
||||||
|
// Auto-apply filter when tag is clicked
|
||||||
|
await this.applyFilters(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
tagsContainer.appendChild(tagEl);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createBaseModelTags() {
|
||||||
|
const baseModelTagsContainer = document.getElementById('baseModelTags');
|
||||||
|
if (!baseModelTagsContainer) return;
|
||||||
|
|
||||||
|
// Fetch base models used in recipes
|
||||||
|
fetch('/api/recipes/base-models')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success && data.base_models) {
|
||||||
|
baseModelTagsContainer.innerHTML = '';
|
||||||
|
|
||||||
|
data.base_models.forEach(model => {
|
||||||
|
const tag = document.createElement('div');
|
||||||
|
tag.className = `filter-tag base-model-tag`;
|
||||||
|
tag.dataset.baseModel = model.name;
|
||||||
|
tag.innerHTML = `${model.name} <span class="tag-count">${model.count}</span>`;
|
||||||
|
|
||||||
|
// Add click handler to toggle selection and automatically apply
|
||||||
|
tag.addEventListener('click', async () => {
|
||||||
|
tag.classList.toggle('active');
|
||||||
|
|
||||||
|
if (tag.classList.contains('active')) {
|
||||||
|
if (!this.filters.baseModel.includes(model.name)) {
|
||||||
|
this.filters.baseModel.push(model.name);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.filters.baseModel = this.filters.baseModel.filter(m => m !== model.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateActiveFiltersCount();
|
||||||
|
|
||||||
|
// Auto-apply filter when tag is clicked
|
||||||
|
await this.applyFilters(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
baseModelTagsContainer.appendChild(tag);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update selections based on stored filters
|
||||||
|
this.updateTagSelections();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error fetching base models:', error);
|
||||||
|
baseModelTagsContainer.innerHTML = '<div class="tags-error">Failed to load base models</div>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleFilterPanel() {
|
||||||
|
if (this.filterPanel) {
|
||||||
|
const isHidden = this.filterPanel.classList.contains('hidden');
|
||||||
|
|
||||||
|
if (isHidden) {
|
||||||
|
// Update panel positions before showing
|
||||||
|
if (window.searchManager && typeof window.searchManager.updatePanelPositions === 'function') {
|
||||||
|
window.searchManager.updatePanelPositions();
|
||||||
|
} else if (typeof updatePanelPositions === 'function') {
|
||||||
|
updatePanelPositions();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.filterPanel.classList.remove('hidden');
|
||||||
|
this.filterButton.classList.add('active');
|
||||||
|
|
||||||
|
// Load tags if they haven't been loaded yet
|
||||||
|
if (!this.tagsLoaded) {
|
||||||
|
this.loadTopTags();
|
||||||
|
this.tagsLoaded = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.closeFilterPanel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closeFilterPanel() {
|
||||||
|
if (this.filterPanel) {
|
||||||
|
this.filterPanel.classList.add('hidden');
|
||||||
|
}
|
||||||
|
if (this.filterButton) {
|
||||||
|
this.filterButton.classList.remove('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTagSelections() {
|
||||||
|
// Update base model tags
|
||||||
|
const baseModelTags = document.querySelectorAll('.base-model-tag');
|
||||||
|
baseModelTags.forEach(tag => {
|
||||||
|
const baseModel = tag.dataset.baseModel;
|
||||||
|
if (this.filters.baseModel.includes(baseModel)) {
|
||||||
|
tag.classList.add('active');
|
||||||
|
} else {
|
||||||
|
tag.classList.remove('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update model tags
|
||||||
|
const modelTags = document.querySelectorAll('.tag-filter');
|
||||||
|
modelTags.forEach(tag => {
|
||||||
|
const tagName = tag.dataset.tag;
|
||||||
|
if (this.filters.tags.includes(tagName)) {
|
||||||
|
tag.classList.add('active');
|
||||||
|
} else {
|
||||||
|
tag.classList.remove('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateActiveFiltersCount() {
|
||||||
|
const totalActiveFilters = this.filters.baseModel.length + this.filters.tags.length;
|
||||||
|
|
||||||
|
if (totalActiveFilters > 0) {
|
||||||
|
this.activeFiltersCount.textContent = totalActiveFilters;
|
||||||
|
this.activeFiltersCount.style.display = 'inline-flex';
|
||||||
|
} else {
|
||||||
|
this.activeFiltersCount.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async applyFilters(showToastNotification = true) {
|
||||||
|
// Save filters to localStorage
|
||||||
|
localStorage.setItem('recipeFilters', JSON.stringify(this.filters));
|
||||||
|
|
||||||
|
// Reload recipes with filters applied
|
||||||
|
if (window.recipeManager && typeof window.recipeManager.loadRecipes === 'function') {
|
||||||
|
try {
|
||||||
|
// Show loading state if available
|
||||||
|
if (window.recipeManager.showLoading) {
|
||||||
|
window.recipeManager.showLoading();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the filters
|
||||||
|
await window.recipeManager.loadRecipes(this.filters);
|
||||||
|
|
||||||
|
// Hide loading state if available
|
||||||
|
if (window.recipeManager.hideLoading) {
|
||||||
|
window.recipeManager.hideLoading();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error applying filters:', error);
|
||||||
|
// Fallback to page reload
|
||||||
|
this._applyFiltersByPageReload();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback to page reload with filter parameters
|
||||||
|
this._applyFiltersByPageReload();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update filter button to show active state
|
||||||
|
if (this.hasActiveFilters()) {
|
||||||
|
this.filterButton.classList.add('active');
|
||||||
|
if (showToastNotification) {
|
||||||
|
const baseModelCount = this.filters.baseModel.length;
|
||||||
|
const tagsCount = this.filters.tags.length;
|
||||||
|
|
||||||
|
let message = '';
|
||||||
|
if (baseModelCount > 0 && tagsCount > 0) {
|
||||||
|
message = `Filtering by ${baseModelCount} base model${baseModelCount > 1 ? 's' : ''} and ${tagsCount} tag${tagsCount > 1 ? 's' : ''}`;
|
||||||
|
} else if (baseModelCount > 0) {
|
||||||
|
message = `Filtering by ${baseModelCount} base model${baseModelCount > 1 ? 's' : ''}`;
|
||||||
|
} else if (tagsCount > 0) {
|
||||||
|
message = `Filtering by ${tagsCount} tag${tagsCount > 1 ? 's' : ''}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast(message, 'success');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.filterButton.classList.remove('active');
|
||||||
|
if (showToastNotification) {
|
||||||
|
showToast('Filters cleared', 'info');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a helper method for page reload fallback
|
||||||
|
_applyFiltersByPageReload() {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
if (this.filters.baseModel.length > 0) {
|
||||||
|
params.append('base_models', this.filters.baseModel.join(','));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.filters.tags.length > 0) {
|
||||||
|
params.append('tags', this.filters.tags.join(','));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.toString()) {
|
||||||
|
window.location.href = `/loras/recipes?${params.toString()}`;
|
||||||
|
} else {
|
||||||
|
window.location.href = '/loras/recipes';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearFilters() {
|
||||||
|
// Clear all filters
|
||||||
|
this.filters = {
|
||||||
|
baseModel: [],
|
||||||
|
tags: []
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update UI
|
||||||
|
this.updateTagSelections();
|
||||||
|
this.updateActiveFiltersCount();
|
||||||
|
|
||||||
|
// Remove from localStorage
|
||||||
|
localStorage.removeItem('recipeFilters');
|
||||||
|
|
||||||
|
// Update UI and reload data
|
||||||
|
this.filterButton.classList.remove('active');
|
||||||
|
|
||||||
|
// Reload recipes without filters
|
||||||
|
if (window.recipeManager && typeof window.recipeManager.loadRecipes === 'function') {
|
||||||
|
await window.recipeManager.loadRecipes();
|
||||||
|
} else {
|
||||||
|
window.location.href = '/loras/recipes';
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast('Recipe filters cleared', 'info');
|
||||||
|
}
|
||||||
|
|
||||||
|
loadFiltersFromStorage() {
|
||||||
|
const savedFilters = localStorage.getItem('recipeFilters');
|
||||||
|
if (savedFilters) {
|
||||||
|
try {
|
||||||
|
const parsedFilters = JSON.parse(savedFilters);
|
||||||
|
|
||||||
|
// Ensure backward compatibility with older filter format
|
||||||
|
this.filters = {
|
||||||
|
baseModel: parsedFilters.baseModel || [],
|
||||||
|
tags: parsedFilters.tags || []
|
||||||
|
};
|
||||||
|
|
||||||
|
this.updateTagSelections();
|
||||||
|
this.updateActiveFiltersCount();
|
||||||
|
|
||||||
|
if (this.hasActiveFilters()) {
|
||||||
|
this.filterButton.classList.add('active');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading recipe filters from storage:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasActiveFilters() {
|
||||||
|
return this.filters.baseModel.length > 0 || this.filters.tags.length > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
154
static/js/managers/SearchManager.js
Normal file
154
static/js/managers/SearchManager.js
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
/**
|
||||||
|
* SearchManager - Handles search functionality across different pages
|
||||||
|
* Each page can extend or customize this base functionality
|
||||||
|
*/
|
||||||
|
export class SearchManager {
|
||||||
|
constructor(options = {}) {
|
||||||
|
this.options = {
|
||||||
|
searchDelay: 300,
|
||||||
|
minSearchLength: 2,
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
|
||||||
|
this.searchInput = document.getElementById('searchInput');
|
||||||
|
this.searchOptionsToggle = document.getElementById('searchOptionsToggle');
|
||||||
|
this.searchOptionsPanel = document.getElementById('searchOptionsPanel');
|
||||||
|
this.closeSearchOptions = document.getElementById('closeSearchOptions');
|
||||||
|
this.searchOptionTags = document.querySelectorAll('.search-option-tag');
|
||||||
|
this.recursiveSearchToggle = document.getElementById('recursiveSearchToggle');
|
||||||
|
|
||||||
|
this.searchTimeout = null;
|
||||||
|
this.currentPage = document.body.dataset.page || 'loras';
|
||||||
|
|
||||||
|
this.initEventListeners();
|
||||||
|
this.loadSearchPreferences();
|
||||||
|
}
|
||||||
|
|
||||||
|
initEventListeners() {
|
||||||
|
// Search input event
|
||||||
|
if (this.searchInput) {
|
||||||
|
this.searchInput.addEventListener('input', () => {
|
||||||
|
clearTimeout(this.searchTimeout);
|
||||||
|
this.searchTimeout = setTimeout(() => this.performSearch(), this.options.searchDelay);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear search with Escape key
|
||||||
|
this.searchInput.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
this.searchInput.value = '';
|
||||||
|
this.performSearch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search options toggle
|
||||||
|
if (this.searchOptionsToggle) {
|
||||||
|
this.searchOptionsToggle.addEventListener('click', () => {
|
||||||
|
this.searchOptionsPanel.classList.toggle('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close search options
|
||||||
|
if (this.closeSearchOptions) {
|
||||||
|
this.closeSearchOptions.addEventListener('click', () => {
|
||||||
|
this.searchOptionsPanel.classList.add('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search option tags
|
||||||
|
if (this.searchOptionTags) {
|
||||||
|
this.searchOptionTags.forEach(tag => {
|
||||||
|
tag.addEventListener('click', () => {
|
||||||
|
tag.classList.toggle('active');
|
||||||
|
this.saveSearchPreferences();
|
||||||
|
this.performSearch();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursive search toggle
|
||||||
|
if (this.recursiveSearchToggle) {
|
||||||
|
this.recursiveSearchToggle.addEventListener('change', () => {
|
||||||
|
this.saveSearchPreferences();
|
||||||
|
this.performSearch();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSearchPreferences() {
|
||||||
|
try {
|
||||||
|
const preferences = JSON.parse(localStorage.getItem(`${this.currentPage}_search_prefs`)) || {};
|
||||||
|
|
||||||
|
// Apply search options
|
||||||
|
if (preferences.options) {
|
||||||
|
this.searchOptionTags.forEach(tag => {
|
||||||
|
const option = tag.dataset.option;
|
||||||
|
if (preferences.options[option] !== undefined) {
|
||||||
|
tag.classList.toggle('active', preferences.options[option]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply recursive search
|
||||||
|
if (this.recursiveSearchToggle && preferences.recursive !== undefined) {
|
||||||
|
this.recursiveSearchToggle.checked = preferences.recursive;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading search preferences:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveSearchPreferences() {
|
||||||
|
try {
|
||||||
|
const options = {};
|
||||||
|
this.searchOptionTags.forEach(tag => {
|
||||||
|
options[tag.dataset.option] = tag.classList.contains('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
const preferences = {
|
||||||
|
options,
|
||||||
|
recursive: this.recursiveSearchToggle ? this.recursiveSearchToggle.checked : false
|
||||||
|
};
|
||||||
|
|
||||||
|
localStorage.setItem(`${this.currentPage}_search_prefs`, JSON.stringify(preferences));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving search preferences:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getActiveSearchOptions() {
|
||||||
|
const options = [];
|
||||||
|
this.searchOptionTags.forEach(tag => {
|
||||||
|
if (tag.classList.contains('active')) {
|
||||||
|
options.push(tag.dataset.option);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
performSearch() {
|
||||||
|
const query = this.searchInput.value.trim();
|
||||||
|
const options = this.getActiveSearchOptions();
|
||||||
|
const recursive = this.recursiveSearchToggle ? this.recursiveSearchToggle.checked : false;
|
||||||
|
|
||||||
|
// This is a base implementation - each page should override this method
|
||||||
|
console.log('Performing search:', {
|
||||||
|
query,
|
||||||
|
options,
|
||||||
|
recursive,
|
||||||
|
page: this.currentPage
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dispatch a custom event that page-specific code can listen for
|
||||||
|
const searchEvent = new CustomEvent('app:search', {
|
||||||
|
detail: {
|
||||||
|
query,
|
||||||
|
options,
|
||||||
|
recursive,
|
||||||
|
page: this.currentPage
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.dispatchEvent(searchEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { initializeCommonComponents } from './common.js';
|
|||||||
import { ImportManager } from './managers/ImportManager.js';
|
import { ImportManager } from './managers/ImportManager.js';
|
||||||
import { RecipeCard } from './components/RecipeCard.js';
|
import { RecipeCard } from './components/RecipeCard.js';
|
||||||
import { RecipeModal } from './components/RecipeModal.js';
|
import { RecipeModal } from './components/RecipeModal.js';
|
||||||
|
import { SearchManager } from './managers/SearchManager.js';
|
||||||
|
|
||||||
class RecipeManager {
|
class RecipeManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -28,6 +29,21 @@ class RecipeManager {
|
|||||||
|
|
||||||
// Load initial set of recipes
|
// Load initial set of recipes
|
||||||
this.loadRecipes();
|
this.loadRecipes();
|
||||||
|
|
||||||
|
// Initialize search manager with Recipe-specific options
|
||||||
|
const recipeSearchManager = new SearchManager({
|
||||||
|
searchCallback: (query, options, recursive) => {
|
||||||
|
// Recipe-specific search implementation
|
||||||
|
fetchRecipes({
|
||||||
|
search: query,
|
||||||
|
search_options: options,
|
||||||
|
recursive: recursive
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set the current page for proper context
|
||||||
|
document.body.dataset.page = 'recipes';
|
||||||
}
|
}
|
||||||
|
|
||||||
initEventListeners() {
|
initEventListeners() {
|
||||||
|
|||||||
@@ -7,21 +7,21 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<nav class="main-nav">
|
<nav class="main-nav">
|
||||||
<a href="/loras" class="nav-item {% if current_page == 'loras' %}active{% endif %}">
|
<a href="/loras" class="nav-item" id="lorasNavItem">
|
||||||
<i class="fas fa-layer-group"></i> LoRAs
|
<i class="fas fa-layer-group"></i> LoRAs
|
||||||
</a>
|
</a>
|
||||||
<a href="/loras/recipes" class="nav-item {% if current_page == 'recipes' %}active{% endif %}">
|
<a href="/loras/recipes" class="nav-item" id="recipesNavItem">
|
||||||
<i class="fas fa-book-open"></i> Recipes
|
<i class="fas fa-book-open"></i> Recipes
|
||||||
</a>
|
</a>
|
||||||
<a href="/checkpoints" class="nav-item {% if current_page == 'checkpoints' %}active{% endif %}">
|
<a href="/checkpoints" class="nav-item" id="checkpointsNavItem">
|
||||||
<i class="fas fa-check-circle"></i> Checkpoints
|
<i class="fas fa-check-circle"></i> Checkpoints
|
||||||
</a>
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- Add search container to header -->
|
<!-- Context-aware search container -->
|
||||||
<div class="header-search">
|
<div class="header-search">
|
||||||
<div class="search-container">
|
<div class="search-container">
|
||||||
<input type="text" id="searchInput" placeholder="Search models..." />
|
<input type="text" id="searchInput" placeholder="Search..." />
|
||||||
<i class="fas fa-search search-icon"></i>
|
<i class="fas fa-search search-icon"></i>
|
||||||
<button class="search-options-toggle" id="searchOptionsToggle" title="Search Options">
|
<button class="search-options-toggle" id="searchOptionsToggle" title="Search Options">
|
||||||
<i class="fas fa-sliders-h"></i>
|
<i class="fas fa-sliders-h"></i>
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Add search options panel -->
|
<!-- Add search options panel with context-aware options -->
|
||||||
<div id="searchOptionsPanel" class="search-options-panel hidden">
|
<div id="searchOptionsPanel" class="search-options-panel hidden">
|
||||||
<div class="options-header">
|
<div class="options-header">
|
||||||
<h3>Search Options</h3>
|
<h3>Search Options</h3>
|
||||||
@@ -67,8 +67,15 @@
|
|||||||
<h4>Search In:</h4>
|
<h4>Search In:</h4>
|
||||||
<div class="search-option-tags">
|
<div class="search-option-tags">
|
||||||
<div class="search-option-tag active" data-option="filename">Filename</div>
|
<div class="search-option-tag active" data-option="filename">Filename</div>
|
||||||
|
{% if request.path == '/loras' or request.path == '/loras/recipes' %}
|
||||||
<div class="search-option-tag active" data-option="tags">Tags</div>
|
<div class="search-option-tag active" data-option="tags">Tags</div>
|
||||||
<div class="search-option-tag active" data-option="modelname">Model Name</div>
|
{% endif %}
|
||||||
|
<div class="search-option-tag active" data-option="modelname">
|
||||||
|
{% if request.path == '/loras/recipes' %}Recipe Name{% elif request.path == '/checkpoints' %}Checkpoint Name{% else %}Model Name{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if request.path == '/loras/recipes' %}
|
||||||
|
<div class="search-option-tag active" data-option="loras">Included LoRAs</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="options-section">
|
<div class="options-section">
|
||||||
@@ -109,3 +116,45 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Add this script at the end of the header component -->
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Get the current path from the URL
|
||||||
|
const currentPath = window.location.pathname;
|
||||||
|
|
||||||
|
// Update search placeholder based on current path
|
||||||
|
const searchInput = document.getElementById('searchInput');
|
||||||
|
if (searchInput) {
|
||||||
|
if (currentPath === '/loras') {
|
||||||
|
searchInput.placeholder = 'Search LoRAs...';
|
||||||
|
} else if (currentPath === '/loras/recipes') {
|
||||||
|
searchInput.placeholder = 'Search recipes...';
|
||||||
|
} else if (currentPath === '/checkpoints') {
|
||||||
|
searchInput.placeholder = 'Search checkpoints...';
|
||||||
|
} else {
|
||||||
|
searchInput.placeholder = 'Search...';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update active nav item
|
||||||
|
const lorasNavItem = document.getElementById('lorasNavItem');
|
||||||
|
const recipesNavItem = document.getElementById('recipesNavItem');
|
||||||
|
const checkpointsNavItem = document.getElementById('checkpointsNavItem');
|
||||||
|
|
||||||
|
const lorasIndicator = document.getElementById('lorasIndicator');
|
||||||
|
const recipesIndicator = document.getElementById('recipesIndicator');
|
||||||
|
const checkpointsIndicator = document.getElementById('checkpointsIndicator');
|
||||||
|
|
||||||
|
if (currentPath === '/loras') {
|
||||||
|
lorasNavItem.classList.add('active');
|
||||||
|
lorasIndicator.style.display = 'block';
|
||||||
|
} else if (currentPath === '/loras/recipes') {
|
||||||
|
recipesNavItem.classList.add('active');
|
||||||
|
recipesIndicator.style.display = 'block';
|
||||||
|
} else if (currentPath === '/checkpoints') {
|
||||||
|
checkpointsNavItem.classList.add('active');
|
||||||
|
checkpointsIndicator.style.display = 'block';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body data-page="loras">
|
||||||
{% include 'components/header.html' %}
|
{% include 'components/header.html' %}
|
||||||
|
|
||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
<!-- Resource loading strategy -->
|
<!-- Resource loading strategy -->
|
||||||
<link rel="preconnect" href="https://cdnjs.cloudflare.com">
|
<link rel="preconnect" href="https://cdnjs.cloudflare.com">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body data-page="recipes">
|
||||||
{% include 'components/header.html' %}
|
{% include 'components/header.html' %}
|
||||||
|
|
||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
@@ -134,6 +134,16 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Recipe page specific scripts -->
|
<!-- Recipe page specific scripts -->
|
||||||
|
<script type="module">
|
||||||
|
import { RecipeFilterManager } from '/loras_static/js/managers/RecipeFilterManager.js';
|
||||||
|
|
||||||
|
// Initialize the recipe filter manager
|
||||||
|
window.recipeFilterManager = new RecipeFilterManager();
|
||||||
|
|
||||||
|
// Make it globally available
|
||||||
|
window.filterManager = window.recipeFilterManager; // For compatibility with existing code
|
||||||
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Refresh recipes
|
// Refresh recipes
|
||||||
function refreshRecipes() {
|
function refreshRecipes() {
|
||||||
@@ -141,29 +151,14 @@
|
|||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Placeholder for recipe filter manager
|
// Import recipes
|
||||||
const recipeFilterManager = {
|
function importRecipes() {
|
||||||
toggleFilterPanel() {
|
// Show import modal
|
||||||
const panel = document.getElementById('filterPanel');
|
const importModal = document.getElementById('importModal');
|
||||||
panel.classList.toggle('hidden');
|
if (importModal) {
|
||||||
},
|
importModal.classList.remove('hidden');
|
||||||
|
}
|
||||||
closeFilterPanel() {
|
|
||||||
document.getElementById('filterPanel').classList.add('hidden');
|
|
||||||
},
|
|
||||||
|
|
||||||
clearFilters() {
|
|
||||||
// Clear filters and reset UI
|
|
||||||
document.querySelectorAll('.filter-tags .tag.active').forEach(tag => {
|
|
||||||
tag.classList.remove('active');
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('activeFiltersCount').style.display = 'none';
|
|
||||||
|
|
||||||
// Reapply default view
|
|
||||||
refreshRecipes();
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user