Add tag filtering checkpoint

This commit is contained in:
Will Miao
2025-03-10 13:18:56 +08:00
parent 0069f84630
commit 721bef3ff8
15 changed files with 482 additions and 50 deletions

View File

@@ -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);
}

View File

@@ -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;
}
}
}

View File

@@ -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());

View File

@@ -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);
}

View File

@@ -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');
}
};

View File

@@ -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;
}
}

View File

@@ -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(),