mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
Add tag filtering checkpoint
This commit is contained in:
@@ -699,4 +699,43 @@
|
||||
[data-theme="dark"] .model-description-content pre,
|
||||
[data-theme="dark"] .model-description-content code {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
/* Model Tags styles */
|
||||
.model-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.model-tag {
|
||||
background: var(--lora-surface);
|
||||
border: 1px solid var(--lora-border);
|
||||
border-radius: var(--border-radius-xs);
|
||||
padding: 3px 8px;
|
||||
font-size: 0.8em;
|
||||
color: var(--lora-accent);
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.model-tag i {
|
||||
font-size: 0.85em;
|
||||
opacity: 0.6;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.model-tag:hover {
|
||||
background: oklch(var(--lora-accent) / 0.1);
|
||||
border-color: var(--lora-accent);
|
||||
}
|
||||
|
||||
.model-tag:hover i {
|
||||
opacity: 1;
|
||||
color: var(--lora-accent);
|
||||
}
|
||||
@@ -237,6 +237,44 @@
|
||||
border-color: var(--lora-accent);
|
||||
}
|
||||
|
||||
/* Tag filter styles */
|
||||
.tag-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.tag-count {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
padding: 1px 6px;
|
||||
border-radius: 10px;
|
||||
font-size: 0.8em;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .tag-count {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.tag-filter.active .tag-count {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tags-loading, .tags-error, .no-tags {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
font-size: 0.9em;
|
||||
color: var(--text-color);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.tags-error {
|
||||
color: var(--lora-error);
|
||||
}
|
||||
|
||||
/* Filter actions */
|
||||
.filter-actions {
|
||||
display: flex;
|
||||
@@ -276,4 +314,4 @@
|
||||
right: 20px;
|
||||
top: 140px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,9 +32,15 @@ export async function loadMoreLoras(boolUpdateFolders = false) {
|
||||
}
|
||||
|
||||
// Add filter parameters if active
|
||||
if (state.filters && state.filters.baseModel && state.filters.baseModel.length > 0) {
|
||||
// Convert the array of base models to a comma-separated string
|
||||
params.append('base_models', state.filters.baseModel.join(','));
|
||||
if (state.filters) {
|
||||
if (state.filters.tags && state.filters.tags.length > 0) {
|
||||
// Convert the array of tags to a comma-separated string
|
||||
params.append('tags', state.filters.tags.join(','));
|
||||
}
|
||||
if (state.filters.baseModel && state.filters.baseModel.length > 0) {
|
||||
// Convert the array of base models to a comma-separated string
|
||||
params.append('base_models', state.filters.baseModel.join(','));
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Loading loras with params:', params.toString());
|
||||
|
||||
@@ -18,6 +18,14 @@ export function createLoraCard(lora) {
|
||||
card.dataset.usage_tips = lora.usage_tips;
|
||||
card.dataset.notes = lora.notes;
|
||||
card.dataset.meta = JSON.stringify(lora.civitai || {});
|
||||
|
||||
// Store tags and model description
|
||||
if (lora.tags && Array.isArray(lora.tags)) {
|
||||
card.dataset.tags = JSON.stringify(lora.tags);
|
||||
}
|
||||
if (lora.modelDescription) {
|
||||
card.dataset.modelDescription = lora.modelDescription;
|
||||
}
|
||||
|
||||
// Apply selection state if in bulk mode and this card is in the selected set
|
||||
if (state.bulkMode && state.selectedLoras.has(lora.file_path)) {
|
||||
@@ -86,7 +94,9 @@ export function createLoraCard(lora) {
|
||||
base_model: card.dataset.base_model,
|
||||
usage_tips: card.dataset.usage_tips,
|
||||
notes: card.dataset.notes,
|
||||
civitai: JSON.parse(card.dataset.meta || '{}')
|
||||
civitai: JSON.parse(card.dataset.meta || '{}'),
|
||||
tags: JSON.parse(card.dataset.tags || '[]'),
|
||||
modelDescription: card.dataset.modelDescription || ''
|
||||
};
|
||||
showLoraModal(loraMeta);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export function showLoraModal(lora) {
|
||||
<i class="fas fa-save"></i>
|
||||
</button>
|
||||
</div>
|
||||
${renderTags(lora.tags || [])}
|
||||
</header>
|
||||
|
||||
<div class="modal-body">
|
||||
@@ -666,4 +667,31 @@ function formatFileSize(bytes) {
|
||||
}
|
||||
|
||||
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to render model tags
|
||||
function renderTags(tags) {
|
||||
if (!tags || tags.length === 0) return '';
|
||||
|
||||
return `
|
||||
<div class="model-tags">
|
||||
${tags.map(tag => `
|
||||
<span class="model-tag" onclick="copyTag('${tag.replace(/'/g, "\\'")}')">
|
||||
${tag}
|
||||
<i class="fas fa-copy"></i>
|
||||
</span>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Add tag copy functionality
|
||||
window.copyTag = async function(tag) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(tag);
|
||||
showToast('Tag copied to clipboard', 'success');
|
||||
} catch (err) {
|
||||
console.error('Copy failed:', err);
|
||||
showToast('Copy failed', 'error');
|
||||
}
|
||||
};
|
||||
@@ -6,7 +6,8 @@ import { resetAndReload } from '../api/loraApi.js';
|
||||
export class FilterManager {
|
||||
constructor() {
|
||||
this.filters = {
|
||||
baseModel: []
|
||||
baseModel: [],
|
||||
tags: []
|
||||
};
|
||||
|
||||
this.filterPanel = document.getElementById('filterPanel');
|
||||
@@ -34,6 +35,77 @@ export class FilterManager {
|
||||
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/top-tags?limit=20');
|
||||
if (!response.ok) throw new Error('Failed to fetch tags');
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Top tags:', data);
|
||||
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 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 tags available</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
tags.forEach(tag => {
|
||||
const tagEl = document.createElement('div');
|
||||
tagEl.className = 'filter-tag tag-filter';
|
||||
// {tag: "name", count: number}
|
||||
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;
|
||||
@@ -69,10 +141,13 @@ export class FilterManager {
|
||||
}
|
||||
|
||||
toggleFilterPanel() {
|
||||
const wasHidden = this.filterPanel.classList.contains('hidden');
|
||||
|
||||
this.filterPanel.classList.toggle('hidden');
|
||||
|
||||
// Mark selected filters
|
||||
if (!this.filterPanel.classList.contains('hidden')) {
|
||||
// If the panel is being opened, load the top tags and update selections
|
||||
if (wasHidden) {
|
||||
this.loadTopTags();
|
||||
this.updateTagSelections();
|
||||
}
|
||||
}
|
||||
@@ -92,10 +167,21 @@ export class FilterManager {
|
||||
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;
|
||||
const totalActiveFilters = this.filters.baseModel.length + this.filters.tags.length;
|
||||
|
||||
if (totalActiveFilters > 0) {
|
||||
this.activeFiltersCount.textContent = totalActiveFilters;
|
||||
@@ -119,7 +205,19 @@ export class FilterManager {
|
||||
if (this.hasActiveFilters()) {
|
||||
this.filterButton.classList.add('active');
|
||||
if (showToastNotification) {
|
||||
showToast(`Filtering by ${this.filters.baseModel.length} base models`, 'success');
|
||||
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');
|
||||
@@ -132,7 +230,8 @@ export class FilterManager {
|
||||
async clearFilters() {
|
||||
// Clear all filters
|
||||
this.filters = {
|
||||
baseModel: []
|
||||
baseModel: [],
|
||||
tags: []
|
||||
};
|
||||
|
||||
// Update state
|
||||
@@ -154,7 +253,14 @@ export class FilterManager {
|
||||
const savedFilters = localStorage.getItem('loraFilters');
|
||||
if (savedFilters) {
|
||||
try {
|
||||
this.filters = JSON.parse(savedFilters);
|
||||
const parsedFilters = JSON.parse(savedFilters);
|
||||
|
||||
// Ensure backward compatibility with older filter format
|
||||
this.filters = {
|
||||
baseModel: parsedFilters.baseModel || [],
|
||||
tags: parsedFilters.tags || []
|
||||
};
|
||||
|
||||
this.updateTagSelections();
|
||||
this.updateActiveFiltersCount();
|
||||
|
||||
@@ -168,6 +274,6 @@ export class FilterManager {
|
||||
}
|
||||
|
||||
hasActiveFilters() {
|
||||
return this.filters.baseModel.length > 0;
|
||||
return this.filters.baseModel.length > 0 || this.filters.tags.length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ export const state = {
|
||||
previewVersions: new Map(),
|
||||
searchManager: null,
|
||||
filters: {
|
||||
baseModel: []
|
||||
baseModel: [],
|
||||
tags: [] // Make sure tags are included in state
|
||||
},
|
||||
bulkMode: false,
|
||||
selectedLoras: new Set(),
|
||||
|
||||
Reference in New Issue
Block a user