feat(bulk-base-model): implement bulk base model setting functionality with UI and context menu integration

This commit is contained in:
Will Miao
2025-09-05 14:07:03 +08:00
parent 3250fa89cb
commit 92ac487128
11 changed files with 390 additions and 26 deletions

View File

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

View File

@@ -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]) => {

View File

@@ -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
*/

View File

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

View File

@@ -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',