From 92ac4871281adeeced3b0dfe8cf099fc6700288b Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Fri, 5 Sep 2025 14:07:03 +0800 Subject: [PATCH] feat(bulk-base-model): implement bulk base model setting functionality with UI and context menu integration --- locales/en.json | 14 ++ locales/zh-CN.json | 14 ++ static/css/components/bulk.css | 58 ++++++++ .../components/ContextMenu/BulkContextMenu.js | 7 + static/js/components/shared/ModelMetadata.js | 28 +--- static/js/managers/BulkManager.js | 125 +++++++++++++++++- static/js/utils/EventManager.js | 110 +++++++++++++++ static/js/utils/constants.js | 23 ++++ templates/components/context_menu.html | 3 + templates/components/modals.html | 3 +- .../modals/bulk_base_model_modal.html | 31 +++++ 11 files changed, 390 insertions(+), 26 deletions(-) create mode 100644 static/js/utils/EventManager.js create mode 100644 templates/components/modals/bulk_base_model_modal.html diff --git a/locales/en.json b/locales/en.json index 652ce18d..b24c5770 100644 --- a/locales/en.json +++ b/locales/en.json @@ -320,6 +320,7 @@ "selectedSuffix": "selected", "viewSelected": "View Selected", "addTags": "Add Tags to All", + "setBaseModel": "Set Base Model for All", "copyAll": "Copy All Syntax", "refreshAll": "Refresh All Metadata", "moveAll": "Move All to Folder", @@ -582,6 +583,14 @@ "replaceTags": "Replace Tags", "saveChanges": "Save changes" }, + "bulkBaseModel": { + "title": "Set Base Model for Multiple Models", + "description": "Set base model for", + "models": "models", + "selectBaseModel": "Select Base Model", + "save": "Update Base Model", + "cancel": "Cancel" + }, "exampleAccess": { "title": "Local Example Images", "message": "No local example images found for this model. View options:", @@ -989,6 +998,11 @@ "nameUpdateFailed": "Failed to update model name", "baseModelUpdated": "Base model updated successfully", "baseModelUpdateFailed": "Failed to update base model", + "baseModelNotSelected": "Please select a base model", + "bulkBaseModelUpdating": "Updating base model for {count} model(s)...", + "bulkBaseModelUpdateSuccess": "Successfully updated base model for {count} model(s)", + "bulkBaseModelUpdatePartial": "Updated {success} model(s), failed {failed} model(s)", + "bulkBaseModelUpdateFailed": "Failed to update base model for selected models", "invalidCharactersRemoved": "Invalid characters removed from filename", "filenameCannotBeEmpty": "File name cannot be empty", "renameFailed": "Failed to rename file: {message}", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 90ca3e48..306e45e4 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -320,6 +320,7 @@ "selectedSuffix": "已选中", "viewSelected": "查看已选中", "addTags": "为所有添加标签", + "setBaseModel": "为所有设置基础模型", "copyAll": "复制全部语法", "refreshAll": "刷新全部元数据", "moveAll": "全部移动到文件夹", @@ -582,6 +583,14 @@ "replaceTags": "替换标签", "saveChanges": "保存更改" }, + "bulkBaseModel": { + "title": "批量设置基础模型", + "description": "为多个模型设置基础模型", + "models": "个模型", + "selectBaseModel": "选择基础模型", + "save": "更新基础模型", + "cancel": "取消" + }, "exampleAccess": { "title": "本地示例图片", "message": "未找到此模型的本地示例图片。可选操作:", @@ -989,6 +998,11 @@ "nameUpdateFailed": "模型名称更新失败", "baseModelUpdated": "基础模型更新成功", "baseModelUpdateFailed": "基础模型更新失败", + "baseModelNotSelected": "请选择基础模型", + "bulkBaseModelUpdating": "正在为 {count} 个模型更新基础模型...", + "bulkBaseModelUpdateSuccess": "成功为 {count} 个模型更新基础模型", + "bulkBaseModelUpdatePartial": "更新了 {success} 个模型,{failed} 个失败", + "bulkBaseModelUpdateFailed": "为选中模型更新基础模型失败", "invalidCharactersRemoved": "文件名中的无效字符已移除", "filenameCannotBeEmpty": "文件名不能为空", "renameFailed": "重命名文件失败:{message}", diff --git a/static/css/components/bulk.css b/static/css/components/bulk.css index b8aa5620..24fcced3 100644 --- a/static/css/components/bulk.css +++ b/static/css/components/bulk.css @@ -46,4 +46,62 @@ -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; +} + +/* Bulk base model modal styles */ +.bulk-base-model-container { + padding: 20px 0; +} + +.bulk-base-model-info { + margin-bottom: 20px; + padding: 15px; + background: var(--lora-surface, #f8f9fa); + border-radius: 8px; + border-left: 4px solid var(--lora-accent, #007bff); +} + +.bulk-base-model-info p { + margin: 0; + color: var(--lora-text-secondary, #6c757d); +} + +.bulk-base-model-selection { + margin-bottom: 25px; +} + +.bulk-base-model-selection label { + display: block; + margin-bottom: 8px; + font-weight: 500; + color: var(--lora-text, #212529); +} + +.bulk-base-model-select { + width: 100%; + padding: 10px 12px; + border: 1px solid var(--lora-border, #dee2e6); + border-radius: 6px; + background: var(--lora-background, #ffffff); + color: var(--lora-text, #212529); + font-size: 14px; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} + +.bulk-base-model-select:focus { + outline: none; + border-color: var(--lora-accent, #007bff); + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +.bulk-base-model-controls { + display: flex; + gap: 12px; + justify-content: flex-end; + padding-top: 20px; + border-top: 1px solid var(--lora-border, #dee2e6); +} + +.bulk-save-base-model-btn { + min-width: 120px; } \ No newline at end of file diff --git a/static/js/components/ContextMenu/BulkContextMenu.js b/static/js/components/ContextMenu/BulkContextMenu.js index 0ef43e74..7c0b42c9 100644 --- a/static/js/components/ContextMenu/BulkContextMenu.js +++ b/static/js/components/ContextMenu/BulkContextMenu.js @@ -27,6 +27,7 @@ export class BulkContextMenu extends BaseContextMenu { // Update button visibility based on model type const addTagsItem = this.menu.querySelector('[data-action="add-tags"]'); + const setBaseModelItem = this.menu.querySelector('[data-action="set-base-model"]'); const sendToWorkflowAppendItem = this.menu.querySelector('[data-action="send-to-workflow-append"]'); const sendToWorkflowReplaceItem = this.menu.querySelector('[data-action="send-to-workflow-replace"]'); const copyAllItem = this.menu.querySelector('[data-action="copy-all"]'); @@ -55,6 +56,9 @@ export class BulkContextMenu extends BaseContextMenu { if (addTagsItem) { addTagsItem.style.display = config.addTags ? 'flex' : 'none'; } + if (setBaseModelItem) { + setBaseModelItem.style.display = 'flex'; // Base model editing is available for all model types + } } updateSelectedCountHeader() { @@ -75,6 +79,9 @@ export class BulkContextMenu extends BaseContextMenu { case 'add-tags': bulkManager.showBulkAddTagsModal(); break; + case 'set-base-model': + bulkManager.showBulkBaseModelModal(); + break; case 'send-to-workflow-append': bulkManager.sendAllModelsToWorkflow(false); break; diff --git a/static/js/components/shared/ModelMetadata.js b/static/js/components/shared/ModelMetadata.js index 9d63ed0b..4dcfab46 100644 --- a/static/js/components/shared/ModelMetadata.js +++ b/static/js/components/shared/ModelMetadata.js @@ -2,10 +2,10 @@ * ModelMetadata.js * Handles model metadata editing functionality - General version */ + +import { BASE_MODEL_CATEGORIES } from '../../utils/constants.js'; import { showToast } from '../../utils/uiHelpers.js'; -import { BASE_MODELS } from '../../utils/constants.js'; import { getModelApiClient } from '../../api/modelApiFactory.js'; -import { translate } from '../../utils/i18nHelpers.js'; /** * Set up model name editing functionality @@ -172,28 +172,8 @@ export function setupBaseModelEditing(filePath) { // Flag to track if a change was made let valueChanged = false; - // Add options from BASE_MODELS constants - const baseModelCategories = { - 'Stable Diffusion 1.x': [BASE_MODELS.SD_1_4, BASE_MODELS.SD_1_5, BASE_MODELS.SD_1_5_LCM, BASE_MODELS.SD_1_5_HYPER], - 'Stable Diffusion 2.x': [BASE_MODELS.SD_2_0, BASE_MODELS.SD_2_1], - 'Stable Diffusion 3.x': [BASE_MODELS.SD_3, BASE_MODELS.SD_3_5, BASE_MODELS.SD_3_5_MEDIUM, BASE_MODELS.SD_3_5_LARGE, BASE_MODELS.SD_3_5_LARGE_TURBO], - 'SDXL': [BASE_MODELS.SDXL, BASE_MODELS.SDXL_LIGHTNING, BASE_MODELS.SDXL_HYPER], - 'Video Models': [ - BASE_MODELS.SVD, BASE_MODELS.LTXV, BASE_MODELS.HUNYUAN_VIDEO, BASE_MODELS.WAN_VIDEO, - BASE_MODELS.WAN_VIDEO_1_3B_T2V, BASE_MODELS.WAN_VIDEO_14B_T2V, - BASE_MODELS.WAN_VIDEO_14B_I2V_480P, BASE_MODELS.WAN_VIDEO_14B_I2V_720P, - BASE_MODELS.WAN_VIDEO_2_2_TI2V_5B, BASE_MODELS.WAN_VIDEO_2_2_T2V_A14B, - BASE_MODELS.WAN_VIDEO_2_2_I2V_A14B - ], - 'Flux Models': [BASE_MODELS.FLUX_1_D, BASE_MODELS.FLUX_1_S, BASE_MODELS.FLUX_1_KONTEXT, BASE_MODELS.FLUX_1_KREA], - 'Other Models': [ - BASE_MODELS.ILLUSTRIOUS, BASE_MODELS.PONY, BASE_MODELS.HIDREAM, - BASE_MODELS.QWEN, BASE_MODELS.AURAFLOW, - BASE_MODELS.PIXART_A, BASE_MODELS.PIXART_E, BASE_MODELS.HUNYUAN_1, - BASE_MODELS.LUMINA, BASE_MODELS.KOLORS, BASE_MODELS.NOOBAI, - BASE_MODELS.UNKNOWN - ] - }; + // Add options from BASE_MODEL_CATEGORIES constants + const baseModelCategories = BASE_MODEL_CATEGORIES; // Create option groups for better organization Object.entries(baseModelCategories).forEach(([category, models]) => { diff --git a/static/js/managers/BulkManager.js b/static/js/managers/BulkManager.js index 5eafa545..e1af4d4f 100644 --- a/static/js/managers/BulkManager.js +++ b/static/js/managers/BulkManager.js @@ -4,7 +4,7 @@ import { updateCardsForBulkMode } from '../components/shared/ModelCard.js'; import { modalManager } from './ModalManager.js'; import { getModelApiClient } from '../api/modelApiFactory.js'; import { MODEL_TYPES, MODEL_CONFIG } from '../api/apiConfig.js'; -import { PRESET_TAGS } from '../utils/constants.js'; +import { PRESET_TAGS, BASE_MODEL_CATEGORIES } from '../utils/constants.js'; export class BulkManager { constructor() { @@ -742,6 +742,129 @@ export class BulkManager { } } + /** + * Show bulk base model modal + */ + showBulkBaseModelModal() { + if (state.selectedModels.size === 0) { + showToast('toast.models.noSelectedModels', {}, 'warning'); + return; + } + + const countElement = document.getElementById('bulkBaseModelCount'); + if (countElement) { + countElement.textContent = state.selectedModels.size; + } + + modalManager.showModal('bulkBaseModelModal', null, null, () => { + this.cleanupBulkBaseModelModal(); + }); + + // Initialize the bulk base model interface + this.initializeBulkBaseModelInterface(); + } + + /** + * Initialize bulk base model interface + */ + initializeBulkBaseModelInterface() { + const select = document.getElementById('bulkBaseModelSelect'); + if (!select) return; + + // Clear existing options + select.innerHTML = ''; + + // Add placeholder option + const placeholderOption = document.createElement('option'); + placeholderOption.value = ''; + placeholderOption.textContent = 'Select a base model...'; + placeholderOption.disabled = true; + placeholderOption.selected = true; + select.appendChild(placeholderOption); + + // Create option groups for better organization + Object.entries(BASE_MODEL_CATEGORIES).forEach(([category, models]) => { + const optgroup = document.createElement('optgroup'); + optgroup.label = category; + + models.forEach(model => { + const option = document.createElement('option'); + option.value = model; + option.textContent = model; + optgroup.appendChild(option); + }); + + select.appendChild(optgroup); + }); + } + + /** + * Save bulk base model changes + */ + async saveBulkBaseModel() { + const select = document.getElementById('bulkBaseModelSelect'); + if (!select || !select.value) { + showToast('toast.models.baseModelNotSelected', {}, 'warning'); + return; + } + + const newBaseModel = select.value; + const selectedCount = state.selectedModels.size; + + if (selectedCount === 0) { + showToast('toast.models.noSelectedModels', {}, 'warning'); + return; + } + + modalManager.closeModal('bulkBaseModelModal'); + + try { + let successCount = 0; + let errorCount = 0; + const errors = []; + + // Show initial progress toast + showToast('toast.models.bulkBaseModelUpdating', { count: selectedCount }, 'info'); + + for (const filepath of state.selectedModels) { + try { + await getModelApiClient().saveModelMetadata(filepath, { base_model: newBaseModel }); + successCount++; + } catch (error) { + errorCount++; + errors.push({ filepath, error: error.message }); + console.error(`Failed to update base model for ${filepath}:`, error); + } + } + + // Show results + if (errorCount === 0) { + showToast('toast.models.bulkBaseModelUpdateSuccess', { count: successCount }, 'success'); + } else if (successCount > 0) { + showToast('toast.models.bulkBaseModelUpdatePartial', { + success: successCount, + failed: errorCount + }, 'warning'); + } else { + showToast('toast.models.bulkBaseModelUpdateFailed', {}, 'error'); + } + + } catch (error) { + console.error('Error during bulk base model operation:', error); + showToast('toast.models.bulkBaseModelUpdateFailed', {}, 'error'); + } + } + + /** + * Cleanup bulk base model modal + */ + cleanupBulkBaseModelModal() { + const select = document.getElementById('bulkBaseModelSelect'); + if (select) { + select.innerHTML = ''; + } + } + /** * Setup marquee selection functionality */ diff --git a/static/js/utils/EventManager.js b/static/js/utils/EventManager.js new file mode 100644 index 00000000..fb60c660 --- /dev/null +++ b/static/js/utils/EventManager.js @@ -0,0 +1,110 @@ +/** + * Centralized manager for handling DOM events across the application + */ +export class EventManager { + constructor() { + // Store registered handlers + this.handlers = new Map(); + // Track active modals/states + this.activeStates = { + bulkMode: false, + marqueeActive: false, + modalOpen: false + }; + } + + /** + * Register an event handler with priority + * @param {string} eventType - The DOM event type (e.g., 'click', 'mousedown') + * @param {string} source - Source identifier (e.g., 'bulkManager', 'contextMenu') + * @param {Function} handler - Event handler function + * @param {Object} options - Additional options including priority (higher number = higher priority) + */ + addHandler(eventType, source, handler, options = {}) { + if (!this.handlers.has(eventType)) { + this.handlers.set(eventType, []); + // Set up the actual DOM listener once + this.setupDOMListener(eventType); + } + + const handlerList = this.handlers.get(eventType); + handlerList.push({ + source, + handler, + priority: options.priority || 0, + options + }); + + // Sort by priority + handlerList.sort((a, b) => b.priority - a.priority); + } + + /** + * Remove an event handler + */ + removeHandler(eventType, source) { + if (!this.handlers.has(eventType)) return; + + const handlerList = this.handlers.get(eventType); + const newList = handlerList.filter(h => h.source !== source); + + if (newList.length === 0) { + // Remove the DOM listener if no handlers remain + this.cleanupDOMListener(eventType); + this.handlers.delete(eventType); + } else { + this.handlers.set(eventType, newList); + } + } + + /** + * Setup actual DOM event listener + */ + setupDOMListener(eventType) { + const listener = (event) => this.handleEvent(eventType, event); + document.addEventListener(eventType, listener); + this._domListeners = this._domListeners || {}; + this._domListeners[eventType] = listener; + } + + /** + * Clean up DOM event listener + */ + cleanupDOMListener(eventType) { + if (this._domListeners && this._domListeners[eventType]) { + document.removeEventListener(eventType, this._domListeners[eventType]); + delete this._domListeners[eventType]; + } + } + + /** + * Process an event through registered handlers + */ + handleEvent(eventType, event) { + if (!this.handlers.has(eventType)) return; + + const handlers = this.handlers.get(eventType); + + for (const {handler, options} of handlers) { + // Apply conditional execution based on app state + if (options.onlyInBulkMode && !this.activeStates.bulkMode) continue; + if (options.onlyWhenMarqueeActive && !this.activeStates.marqueeActive) continue; + if (options.skipWhenModalOpen && this.activeStates.modalOpen) continue; + + // Execute handler + const result = handler(event); + + // Stop propagation if handler returns true + if (result === true) break; + } + } + + /** + * Update application state + */ + setState(state, value) { + this.activeStates[state] = value; + } +} + +export const eventManager = new EventManager(); \ No newline at end of file diff --git a/static/js/utils/constants.js b/static/js/utils/constants.js index 196de1fc..fa62dce7 100644 --- a/static/js/utils/constants.js +++ b/static/js/utils/constants.js @@ -164,6 +164,29 @@ export const NODE_TYPE_ICONS = { // Default ComfyUI node color when bgcolor is null export const DEFAULT_NODE_COLOR = "#353535"; +// Base model categories for organized selection +export const BASE_MODEL_CATEGORIES = { + 'Stable Diffusion 1.x': [BASE_MODELS.SD_1_4, BASE_MODELS.SD_1_5, BASE_MODELS.SD_1_5_LCM, BASE_MODELS.SD_1_5_HYPER], + 'Stable Diffusion 2.x': [BASE_MODELS.SD_2_0, BASE_MODELS.SD_2_1], + 'Stable Diffusion 3.x': [BASE_MODELS.SD_3, BASE_MODELS.SD_3_5, BASE_MODELS.SD_3_5_MEDIUM, BASE_MODELS.SD_3_5_LARGE, BASE_MODELS.SD_3_5_LARGE_TURBO], + 'SDXL': [BASE_MODELS.SDXL, BASE_MODELS.SDXL_LIGHTNING, BASE_MODELS.SDXL_HYPER], + 'Video Models': [ + BASE_MODELS.SVD, BASE_MODELS.LTXV, BASE_MODELS.HUNYUAN_VIDEO, BASE_MODELS.WAN_VIDEO, + BASE_MODELS.WAN_VIDEO_1_3B_T2V, BASE_MODELS.WAN_VIDEO_14B_T2V, + BASE_MODELS.WAN_VIDEO_14B_I2V_480P, BASE_MODELS.WAN_VIDEO_14B_I2V_720P, + BASE_MODELS.WAN_VIDEO_2_2_TI2V_5B, BASE_MODELS.WAN_VIDEO_2_2_T2V_A14B, + BASE_MODELS.WAN_VIDEO_2_2_I2V_A14B + ], + 'Flux Models': [BASE_MODELS.FLUX_1_D, BASE_MODELS.FLUX_1_S, BASE_MODELS.FLUX_1_KONTEXT, BASE_MODELS.FLUX_1_KREA], + 'Other Models': [ + BASE_MODELS.ILLUSTRIOUS, BASE_MODELS.PONY, BASE_MODELS.HIDREAM, + BASE_MODELS.QWEN, BASE_MODELS.AURAFLOW, + BASE_MODELS.PIXART_A, BASE_MODELS.PIXART_E, BASE_MODELS.HUNYUAN_1, + BASE_MODELS.LUMINA, BASE_MODELS.KOLORS, BASE_MODELS.NOOBAI, + BASE_MODELS.UNKNOWN + ] +}; + // Preset tag suggestions export const PRESET_TAGS = [ 'character', 'style', 'concept', 'clothing', diff --git a/templates/components/context_menu.html b/templates/components/context_menu.html index 45868caf..52187615 100644 --- a/templates/components/context_menu.html +++ b/templates/components/context_menu.html @@ -53,6 +53,9 @@
{{ t('loras.bulkOperations.addTags') }}
+
+ {{ t('loras.bulkOperations.setBaseModel') }} +
{{ t('loras.contextMenu.sendToWorkflowAppend') }}
diff --git a/templates/components/modals.html b/templates/components/modals.html index 14068b44..3f0e8a14 100644 --- a/templates/components/modals.html +++ b/templates/components/modals.html @@ -10,4 +10,5 @@ {% include 'components/modals/example_access_modal.html' %} {% include 'components/modals/download_modal.html' %} {% include 'components/modals/move_modal.html' %} -{% include 'components/modals/bulk_add_tags_modal.html' %} \ No newline at end of file +{% include 'components/modals/bulk_add_tags_modal.html' %} +{% include 'components/modals/bulk_base_model_modal.html' %} \ No newline at end of file diff --git a/templates/components/modals/bulk_base_model_modal.html b/templates/components/modals/bulk_base_model_modal.html new file mode 100644 index 00000000..3e705910 --- /dev/null +++ b/templates/components/modals/bulk_base_model_modal.html @@ -0,0 +1,31 @@ +