diff --git a/static/js/components/ContextMenu/BulkContextMenu.js b/static/js/components/ContextMenu/BulkContextMenu.js index 7e228641..70030764 100644 --- a/static/js/components/ContextMenu/BulkContextMenu.js +++ b/static/js/components/ContextMenu/BulkContextMenu.js @@ -26,6 +26,7 @@ export class BulkContextMenu extends BaseContextMenu { if (!config) return; // Update button visibility based on model type + const addTagsItem = this.menu.querySelector('[data-action="add-tags"]'); const sendToWorkflowItem = this.menu.querySelector('[data-action="send-to-workflow"]'); const copyAllItem = this.menu.querySelector('[data-action="copy-all"]'); const refreshAllItem = this.menu.querySelector('[data-action="refresh-all"]'); @@ -47,6 +48,9 @@ export class BulkContextMenu extends BaseContextMenu { if (deleteAllItem) { deleteAllItem.style.display = config.deleteAll ? 'flex' : 'none'; } + if (addTagsItem) { + addTagsItem.style.display = config.addTags ? 'flex' : 'none'; + } } updateSelectedCountHeader() { @@ -64,6 +68,9 @@ export class BulkContextMenu extends BaseContextMenu { handleMenuAction(action, menuItem) { switch (action) { + case 'add-tags': + bulkManager.showBulkAddTagsModal(); + break; case 'send-to-workflow': bulkManager.sendAllModelsToWorkflow(); break; diff --git a/static/js/components/shared/ModelTags.js b/static/js/components/shared/ModelTags.js index 87abe0ce..ca958d6a 100644 --- a/static/js/components/shared/ModelTags.js +++ b/static/js/components/shared/ModelTags.js @@ -139,7 +139,7 @@ export function setupTagEditMode() { // ...existing helper functions... /** - * Save tags - 支持LoRA和Checkpoint + * Save tags */ async function saveTags() { const editBtn = document.querySelector('.edit-tags-btn'); diff --git a/static/js/managers/BulkManager.js b/static/js/managers/BulkManager.js index 8946bc11..052163c7 100644 --- a/static/js/managers/BulkManager.js +++ b/static/js/managers/BulkManager.js @@ -14,6 +14,7 @@ export class BulkManager { // Model type specific action configurations this.actionConfig = { [MODEL_TYPES.LORA]: { + addTags: true, sendToWorkflow: true, copyAll: true, refreshAll: true, @@ -21,6 +22,7 @@ export class BulkManager { deleteAll: true }, [MODEL_TYPES.EMBEDDING]: { + addTags: true, sendToWorkflow: false, copyAll: false, refreshAll: true, @@ -28,6 +30,7 @@ export class BulkManager { deleteAll: true }, [MODEL_TYPES.CHECKPOINT]: { + addTags: true, sendToWorkflow: false, copyAll: false, refreshAll: true, @@ -406,6 +409,225 @@ export class BulkManager { showToast('toast.models.refreshMetadataFailed', {}, 'error'); } } + + showBulkAddTagsModal() { + if (state.selectedModels.size === 0) { + showToast('toast.models.noModelsSelected', {}, 'warning'); + return; + } + + const countElement = document.getElementById('bulkAddTagsCount'); + if (countElement) { + countElement.textContent = state.selectedModels.size; + } + + // Clear any existing tags in the modal + const tagsContainer = document.getElementById('bulkTagsItems'); + if (tagsContainer) { + tagsContainer.innerHTML = ''; + } + + modalManager.showModal('bulkAddTagsModal', null, null, () => { + // Cleanup when modal is closed + this.cleanupBulkAddTagsModal(); + }); + + // Initialize the bulk tags editing interface + this.initializeBulkTagsInterface(); + } + + initializeBulkTagsInterface() { + // Import preset tags from ModelTags.js + const PRESET_TAGS = [ + 'character', 'style', 'concept', 'clothing', + 'poses', 'background', 'vehicle', 'buildings', + 'objects', 'animal' + ]; + + // Setup tag input behavior + const tagInput = document.querySelector('.bulk-metadata-input'); + if (tagInput) { + tagInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + this.addBulkTag(e.target.value.trim()); + e.target.value = ''; + } + }); + } + + // Create suggestions dropdown + const tagForm = document.querySelector('#bulkAddTagsModal .metadata-add-form'); + if (tagForm) { + const suggestionsDropdown = this.createBulkSuggestionsDropdown(PRESET_TAGS); + tagForm.appendChild(suggestionsDropdown); + } + + // Setup save button + const saveBtn = document.querySelector('.bulk-save-tags-btn'); + if (saveBtn) { + saveBtn.addEventListener('click', () => { + this.saveBulkTags(); + }); + } + } + + createBulkSuggestionsDropdown(presetTags) { + const dropdown = document.createElement('div'); + dropdown.className = 'metadata-suggestions-dropdown'; + + const header = document.createElement('div'); + header.className = 'metadata-suggestions-header'; + header.innerHTML = ` + Suggested Tags + Click to add + `; + dropdown.appendChild(header); + + const container = document.createElement('div'); + container.className = 'metadata-suggestions-container'; + + presetTags.forEach(tag => { + const item = document.createElement('div'); + item.className = 'metadata-suggestion-item'; + item.title = tag; + item.innerHTML = `${tag}`; + + item.addEventListener('click', () => { + this.addBulkTag(tag); + const input = document.querySelector('.bulk-metadata-input'); + if (input) { + input.value = tag; + input.focus(); + } + }); + + container.appendChild(item); + }); + + dropdown.appendChild(container); + return dropdown; + } + + addBulkTag(tag) { + tag = tag.trim().toLowerCase(); + if (!tag) return; + + const tagsContainer = document.getElementById('bulkTagsItems'); + if (!tagsContainer) return; + + // Validation: Check length + if (tag.length > 30) { + showToast('modelTags.validation.maxLength', {}, 'error'); + return; + } + + // Validation: Check total number + const currentTags = tagsContainer.querySelectorAll('.metadata-item'); + if (currentTags.length >= 30) { + showToast('modelTags.validation.maxCount', {}, 'error'); + return; + } + + // Validation: Check for duplicates + const existingTags = Array.from(currentTags).map(tagEl => tagEl.dataset.tag); + if (existingTags.includes(tag)) { + showToast('modelTags.validation.duplicate', {}, 'error'); + return; + } + + // Create new tag + const newTag = document.createElement('div'); + newTag.className = 'metadata-item'; + newTag.dataset.tag = tag; + newTag.innerHTML = ` + ${tag} + + `; + + // Add delete button event listener + const deleteBtn = newTag.querySelector('.metadata-delete-btn'); + deleteBtn.addEventListener('click', (e) => { + e.stopPropagation(); + newTag.remove(); + }); + + tagsContainer.appendChild(newTag); + } + + async saveBulkTags() { + const tagElements = document.querySelectorAll('#bulkTagsItems .metadata-item'); + const tags = Array.from(tagElements).map(tag => tag.dataset.tag); + + if (tags.length === 0) { + showToast('toast.models.noTagsToAdd', {}, 'warning'); + return; + } + + if (state.selectedModels.size === 0) { + showToast('toast.models.noModelsSelected', {}, 'warning'); + return; + } + + try { + const apiClient = getModelApiClient(); + const filePaths = Array.from(state.selectedModels); + let successCount = 0; + let failCount = 0; + + // Add tags to each selected model + for (const filePath of filePaths) { + try { + await apiClient.addTags(filePath, { tags: tags }); + successCount++; + } catch (error) { + console.error(`Failed to add tags to ${filePath}:`, error); + failCount++; + } + } + + modalManager.closeModal('bulkAddTagsModal'); + + if (successCount > 0) { + const currentConfig = MODEL_CONFIG[state.currentPageType]; + showToast('toast.models.tagsAddedSuccessfully', { + count: successCount, + tagCount: tags.length, + type: currentConfig.displayName.toLowerCase() + }, 'success'); + } + + if (failCount > 0) { + showToast('toast.models.tagsAddFailed', { count: failCount }, 'warning'); + } + + } catch (error) { + console.error('Error during bulk tag addition:', error); + showToast('toast.models.bulkTagsAddFailed', {}, 'error'); + } + } + + cleanupBulkAddTagsModal() { + // Clear tags container + const tagsContainer = document.getElementById('bulkTagsItems'); + if (tagsContainer) { + tagsContainer.innerHTML = ''; + } + + // Clear input + const input = document.querySelector('.bulk-metadata-input'); + if (input) { + input.value = ''; + } + + // Remove event listeners (they will be re-added when modal opens again) + const saveBtn = document.querySelector('.bulk-save-tags-btn'); + if (saveBtn) { + saveBtn.replaceWith(saveBtn.cloneNode(true)); + } + } } export const bulkManager = new BulkManager(); diff --git a/static/js/managers/ModalManager.js b/static/js/managers/ModalManager.js index 9f56c269..8313c226 100644 --- a/static/js/managers/ModalManager.js +++ b/static/js/managers/ModalManager.js @@ -234,6 +234,19 @@ export class ModalManager { }); } + // Add bulkAddTagsModal registration + const bulkAddTagsModal = document.getElementById('bulkAddTagsModal'); + if (bulkAddTagsModal) { + this.registerModal('bulkAddTagsModal', { + element: bulkAddTagsModal, + onClose: () => { + this.getModal('bulkAddTagsModal').element.style.display = 'none'; + document.body.classList.remove('modal-open'); + }, + closeOnOutsideClick: true + }); + } + document.addEventListener('keydown', this.boundHandleEscape); this.initialized = true; } diff --git a/templates/components/context_menu.html b/templates/components/context_menu.html index b9cb211a..92e7a5b2 100644 --- a/templates/components/context_menu.html +++ b/templates/components/context_menu.html @@ -50,6 +50,9 @@ {{ t('loras.bulkOperations.selected', {'count': 0}) }}
+
+ {{ t('loras.bulkOperations.addTags') }} +
{{ t('loras.bulkOperations.sendToWorkflow') }}
diff --git a/templates/components/modals.html b/templates/components/modals.html index 0b003976..14068b44 100644 --- a/templates/components/modals.html +++ b/templates/components/modals.html @@ -9,4 +9,5 @@ {% include 'components/modals/relink_civitai_modal.html' %} {% include 'components/modals/example_access_modal.html' %} {% include 'components/modals/download_modal.html' %} -{% include 'components/modals/move_modal.html' %} \ No newline at end of file +{% include 'components/modals/move_modal.html' %} +{% include 'components/modals/bulk_add_tags_modal.html' %} \ No newline at end of file diff --git a/templates/components/modals/bulk_add_tags_modal.html b/templates/components/modals/bulk_add_tags_modal.html new file mode 100644 index 00000000..7538ec89 --- /dev/null +++ b/templates/components/modals/bulk_add_tags_modal.html @@ -0,0 +1,37 @@ +