mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -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(
|
||||
folders=[], # 空文件夹列表
|
||||
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:
|
||||
# 正常流程
|
||||
@@ -80,7 +81,8 @@ class LoraRoutes:
|
||||
rendered = template.render(
|
||||
folders=cache.folders,
|
||||
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(
|
||||
@@ -110,7 +112,8 @@ class LoraRoutes:
|
||||
template = self.template_env.get_template('recipes.html')
|
||||
rendered = template.render(
|
||||
is_initializing=True,
|
||||
settings=settings
|
||||
settings=settings,
|
||||
request=request # Pass the request object to the template
|
||||
)
|
||||
else:
|
||||
# Normal flow - get recipes with the same formatting as the API endpoint
|
||||
@@ -137,7 +140,8 @@ class LoraRoutes:
|
||||
rendered = template.render(
|
||||
recipes=recipes_data,
|
||||
is_initializing=False,
|
||||
settings=settings
|
||||
settings=settings,
|
||||
request=request # Pass the request object to the template
|
||||
)
|
||||
|
||||
return web.Response(
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
} from './utils/uiHelpers.js';
|
||||
import { initializeInfiniteScroll } from './utils/infiniteScroll.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 { SettingsManager, toggleApiKeyVisibility } from './managers/SettingsManager.js';
|
||||
import { LoraContextMenu } from './components/ContextMenu.js';
|
||||
@@ -104,6 +104,22 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
|
||||
// Update positions on window resize
|
||||
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
|
||||
|
||||
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 { RecipeCard } from './components/RecipeCard.js';
|
||||
import { RecipeModal } from './components/RecipeModal.js';
|
||||
import { SearchManager } from './managers/SearchManager.js';
|
||||
|
||||
class RecipeManager {
|
||||
constructor() {
|
||||
@@ -28,6 +29,21 @@ class RecipeManager {
|
||||
|
||||
// Load initial set of recipes
|
||||
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() {
|
||||
|
||||
@@ -7,21 +7,21 @@
|
||||
</a>
|
||||
</div>
|
||||
<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
|
||||
</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
|
||||
</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
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<!-- Add search container to header -->
|
||||
<!-- Context-aware search container -->
|
||||
<div class="header-search">
|
||||
<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>
|
||||
<button class="search-options-toggle" id="searchOptionsToggle" title="Search Options">
|
||||
<i class="fas fa-sliders-h"></i>
|
||||
@@ -55,7 +55,7 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Add search options panel -->
|
||||
<!-- Add search options panel with context-aware options -->
|
||||
<div id="searchOptionsPanel" class="search-options-panel hidden">
|
||||
<div class="options-header">
|
||||
<h3>Search Options</h3>
|
||||
@@ -67,8 +67,15 @@
|
||||
<h4>Search In:</h4>
|
||||
<div class="search-option-tags">
|
||||
<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="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 class="options-section">
|
||||
@@ -108,4 +115,46 @@
|
||||
Clear All Filters
|
||||
</button>
|
||||
</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>
|
||||
</head>
|
||||
<body>
|
||||
<body data-page="loras">
|
||||
{% include 'components/header.html' %}
|
||||
|
||||
<div class="page-content">
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<!-- Resource loading strategy -->
|
||||
<link rel="preconnect" href="https://cdnjs.cloudflare.com">
|
||||
</head>
|
||||
<body>
|
||||
<body data-page="recipes">
|
||||
{% include 'components/header.html' %}
|
||||
|
||||
<div class="page-content">
|
||||
@@ -134,6 +134,16 @@
|
||||
{% endif %}
|
||||
|
||||
<!-- 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>
|
||||
// Refresh recipes
|
||||
function refreshRecipes() {
|
||||
@@ -141,29 +151,14 @@
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
// Placeholder for recipe filter manager
|
||||
const recipeFilterManager = {
|
||||
toggleFilterPanel() {
|
||||
const panel = document.getElementById('filterPanel');
|
||||
panel.classList.toggle('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();
|
||||
// Import recipes
|
||||
function importRecipes() {
|
||||
// Show import modal
|
||||
const importModal = document.getElementById('importModal');
|
||||
if (importModal) {
|
||||
importModal.classList.remove('hidden');
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user