mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-24 06:32:12 -03:00
feat(auto-organize): add auto-organize functionality for selected models and update context menu
This commit is contained in:
@@ -94,6 +94,10 @@ export function getApiEndpoints(modelType) {
|
||||
metadata: `/api/${modelType}/metadata`,
|
||||
modelDescription: `/api/${modelType}/model-description`,
|
||||
|
||||
// Auto-organize operations
|
||||
autoOrganize: `/api/${modelType}/auto-organize`,
|
||||
autoOrganizeProgress: `/api/${modelType}/auto-organize-progress`,
|
||||
|
||||
// Model-specific endpoints (will be merged with specific configs)
|
||||
specific: {}
|
||||
};
|
||||
|
||||
@@ -1011,23 +1011,140 @@ export class BaseModelApiClient {
|
||||
|
||||
async fetchModelDescription(filePath) {
|
||||
try {
|
||||
const params = new URLSearchParams({ file_path: filePath });
|
||||
const response = await fetch(`${this.apiConfig.endpoints.modelDescription}?${params}`);
|
||||
|
||||
const response = await fetch(`${this.apiConfig.endpoints.modelDescription}?file_path=${encodeURIComponent(filePath)}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch ${this.apiConfig.config.singularName} description: ${response.statusText}`);
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
return data.description;
|
||||
} else {
|
||||
throw new Error(data.error || `No description found for ${this.apiConfig.config.singularName}`);
|
||||
}
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching ${this.apiConfig.config.singularName} description:`, error);
|
||||
console.error('Error fetching model description:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-organize models based on current path template settings
|
||||
* @param {Array} filePaths - Optional array of file paths to organize. If not provided, organizes all models.
|
||||
* @returns {Promise} - Promise that resolves when the operation is complete
|
||||
*/
|
||||
async autoOrganizeModels(filePaths = null) {
|
||||
let ws = null;
|
||||
|
||||
await state.loadingManager.showWithProgress(async (loading) => {
|
||||
try {
|
||||
// Connect to WebSocket for progress updates
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||
ws = new WebSocket(`${wsProtocol}${window.location.host}${WS_ENDPOINTS.fetchProgress}`);
|
||||
|
||||
const operationComplete = new Promise((resolve, reject) => {
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
if (data.type !== 'auto_organize_progress') return;
|
||||
|
||||
switch(data.status) {
|
||||
case 'started':
|
||||
loading.setProgress(0);
|
||||
const operationType = data.operation_type === 'bulk' ? 'selected models' : 'all models';
|
||||
loading.setStatus(translate('loras.bulkOperations.autoOrganizeProgress.starting', { type: operationType }, `Starting auto-organize for ${operationType}...`));
|
||||
break;
|
||||
|
||||
case 'processing':
|
||||
const percent = data.total > 0 ? ((data.processed / data.total) * 90).toFixed(1) : 0;
|
||||
loading.setProgress(percent);
|
||||
loading.setStatus(
|
||||
translate('loras.bulkOperations.autoOrganizeProgress.processing', {
|
||||
processed: data.processed,
|
||||
total: data.total,
|
||||
success: data.success,
|
||||
failures: data.failures,
|
||||
skipped: data.skipped
|
||||
}, `Processing (${data.processed}/${data.total}) - ${data.success} moved, ${data.skipped} skipped, ${data.failures} failed`)
|
||||
);
|
||||
break;
|
||||
|
||||
case 'cleaning':
|
||||
loading.setProgress(95);
|
||||
loading.setStatus(translate('loras.bulkOperations.autoOrganizeProgress.cleaning', {}, 'Cleaning up empty directories...'));
|
||||
break;
|
||||
|
||||
case 'completed':
|
||||
loading.setProgress(100);
|
||||
loading.setStatus(
|
||||
translate('loras.bulkOperations.autoOrganizeProgress.completed', {
|
||||
success: data.success,
|
||||
skipped: data.skipped,
|
||||
failures: data.failures,
|
||||
total: data.total
|
||||
}, `Completed: ${data.success} moved, ${data.skipped} skipped, ${data.failures} failed`)
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
resolve(data);
|
||||
}, 1500);
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
loading.setStatus(translate('loras.bulkOperations.autoOrganizeProgress.error', { error: data.error }, `Error: ${data.error}`));
|
||||
reject(new Error(data.error));
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket error during auto-organize:', error);
|
||||
reject(new Error('Connection error'));
|
||||
};
|
||||
});
|
||||
|
||||
// Start the auto-organize operation
|
||||
const endpoint = this.apiConfig.endpoints.autoOrganize;
|
||||
const requestOptions = {
|
||||
method: filePaths ? 'POST' : 'GET',
|
||||
headers: filePaths ? { 'Content-Type': 'application/json' } : {}
|
||||
};
|
||||
|
||||
if (filePaths) {
|
||||
requestOptions.body = JSON.stringify({ file_paths: filePaths });
|
||||
}
|
||||
|
||||
const response = await fetch(endpoint, requestOptions);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.error || 'Failed to start auto-organize operation');
|
||||
}
|
||||
|
||||
// Wait for the operation to complete via WebSocket
|
||||
const result = await operationComplete;
|
||||
|
||||
// Show appropriate success message based on results
|
||||
if (result.failures === 0) {
|
||||
showToast('toast.loras.autoOrganizeSuccess', {
|
||||
count: result.success,
|
||||
type: result.operation_type === 'bulk' ? 'selected models' : 'all models'
|
||||
}, 'success');
|
||||
} else {
|
||||
showToast('toast.loras.autoOrganizePartialSuccess', {
|
||||
success: result.success,
|
||||
failures: result.failures,
|
||||
total: result.total
|
||||
}, 'warning');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error during auto-organize:', error);
|
||||
showToast('toast.loras.autoOrganizeFailed', { error: error.message }, 'error');
|
||||
throw error;
|
||||
} finally {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.close();
|
||||
}
|
||||
}
|
||||
}, {
|
||||
initialMessage: translate('loras.bulkOperations.autoOrganizeProgress.initializing', {}, 'Initializing auto-organize...'),
|
||||
completionMessage: translate('loras.bulkOperations.autoOrganizeProgress.complete', {}, 'Auto-organize complete')
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ export class BulkContextMenu extends BaseContextMenu {
|
||||
const copyAllItem = this.menu.querySelector('[data-action="copy-all"]');
|
||||
const refreshAllItem = this.menu.querySelector('[data-action="refresh-all"]');
|
||||
const moveAllItem = this.menu.querySelector('[data-action="move-all"]');
|
||||
const autoOrganizeItem = this.menu.querySelector('[data-action="auto-organize"]');
|
||||
const deleteAllItem = this.menu.querySelector('[data-action="delete-all"]');
|
||||
|
||||
if (sendToWorkflowAppendItem) {
|
||||
@@ -50,6 +51,9 @@ export class BulkContextMenu extends BaseContextMenu {
|
||||
if (moveAllItem) {
|
||||
moveAllItem.style.display = config.moveAll ? 'flex' : 'none';
|
||||
}
|
||||
if (autoOrganizeItem) {
|
||||
autoOrganizeItem.style.display = config.autoOrganize ? 'flex' : 'none';
|
||||
}
|
||||
if (deleteAllItem) {
|
||||
deleteAllItem.style.display = config.deleteAll ? 'flex' : 'none';
|
||||
}
|
||||
@@ -97,6 +101,9 @@ export class BulkContextMenu extends BaseContextMenu {
|
||||
case 'move-all':
|
||||
window.moveManager.showMoveModal('bulk');
|
||||
break;
|
||||
case 'auto-organize':
|
||||
bulkManager.autoOrganizeSelectedModels();
|
||||
break;
|
||||
case 'delete-all':
|
||||
bulkManager.showBulkDeleteModal();
|
||||
break;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { state, getCurrentPageState } from '../state/index.js';
|
||||
import { showToast, copyToClipboard, sendLoraToWorkflow } from '../utils/uiHelpers.js';
|
||||
import { updateCardsForBulkMode } from '../components/shared/ModelCard.js';
|
||||
import { modalManager } from './ModalManager.js';
|
||||
import { getModelApiClient } from '../api/modelApiFactory.js';
|
||||
import { getModelApiClient, resetAndReload } from '../api/modelApiFactory.js';
|
||||
import { MODEL_TYPES, MODEL_CONFIG } from '../api/apiConfig.js';
|
||||
import { PRESET_TAGS, BASE_MODEL_CATEGORIES } from '../utils/constants.js';
|
||||
import { eventManager } from '../utils/EventManager.js';
|
||||
@@ -34,6 +34,7 @@ export class BulkManager {
|
||||
copyAll: true,
|
||||
refreshAll: true,
|
||||
moveAll: true,
|
||||
autoOrganize: true,
|
||||
deleteAll: true
|
||||
},
|
||||
[MODEL_TYPES.EMBEDDING]: {
|
||||
@@ -42,6 +43,7 @@ export class BulkManager {
|
||||
copyAll: false,
|
||||
refreshAll: true,
|
||||
moveAll: true,
|
||||
autoOrganize: true,
|
||||
deleteAll: true
|
||||
},
|
||||
[MODEL_TYPES.CHECKPOINT]: {
|
||||
@@ -50,6 +52,7 @@ export class BulkManager {
|
||||
copyAll: false,
|
||||
refreshAll: true,
|
||||
moveAll: false,
|
||||
autoOrganize: true,
|
||||
deleteAll: true
|
||||
}
|
||||
};
|
||||
@@ -956,9 +959,48 @@ export class BulkManager {
|
||||
* Cleanup bulk base model modal
|
||||
*/
|
||||
cleanupBulkBaseModelModal() {
|
||||
const select = document.getElementById('bulkBaseModelSelect');
|
||||
if (select) {
|
||||
select.innerHTML = '';
|
||||
const modal = document.getElementById('bulkBaseModelModal');
|
||||
if (modal) {
|
||||
// Clear existing tags
|
||||
const tagsContainer = modal.querySelector('.bulk-tags');
|
||||
if (tagsContainer) {
|
||||
tagsContainer.innerHTML = '';
|
||||
}
|
||||
|
||||
// Clear dropdown
|
||||
const dropdown = modal.querySelector('.bulk-suggestions-dropdown');
|
||||
if (dropdown) {
|
||||
dropdown.innerHTML = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-organize selected models based on current path template settings
|
||||
*/
|
||||
async autoOrganizeSelectedModels() {
|
||||
if (state.selectedModels.size === 0) {
|
||||
showToast('toast.loras.noModelsSelected', {}, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get selected file paths
|
||||
const filePaths = Array.from(state.selectedModels);
|
||||
|
||||
// Get the API client for the current model type
|
||||
const apiClient = getModelApiClient();
|
||||
|
||||
// Call the auto-organize method with selected file paths
|
||||
await apiClient.autoOrganizeModels(filePaths);
|
||||
|
||||
setTimeout(() => {
|
||||
resetAndReload(true);
|
||||
}, 1000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error during bulk auto-organize:', error);
|
||||
showToast('toast.loras.autoOrganizeFailed', { error: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user