feat(auto-organize): add auto-organize functionality for selected models and update context menu

This commit is contained in:
Will Miao
2025-09-05 20:51:30 +08:00
parent 5e69671366
commit c109e392ad
15 changed files with 374 additions and 46 deletions

View File

@@ -324,8 +324,18 @@
"copyAll": "Alle Syntax kopieren",
"refreshAll": "Alle Metadaten aktualisieren",
"moveAll": "Alle in Ordner verschieben",
"autoOrganize": "Automatisch organisieren",
"deleteAll": "Alle Modelle löschen",
"clear": "Auswahl löschen"
"clear": "Auswahl löschen",
"autoOrganizeProgress": {
"initializing": "Automatische Organisation wird initialisiert...",
"starting": "Automatische Organisation für {type} wird gestartet...",
"processing": "Verarbeitung ({processed}/{total}) {success} verschoben, {skipped} übersprungen, {failures} fehlgeschlagen",
"cleaning": "Leere Verzeichnisse werden bereinigt...",
"completed": "Abgeschlossen: {success} verschoben, {skipped} übersprungen, {failures} fehlgeschlagen",
"complete": "Automatische Organisation abgeschlossen",
"error": "Fehler: {error}"
}
},
"contextMenu": {
"refreshMetadata": "Civitai-Daten aktualisieren",
@@ -942,7 +952,11 @@
"downloadPartialWithAccess": "{completed} von {total} LoRAs heruntergeladen. {accessFailures} fehlgeschlagen aufgrund von Zugriffsbeschränkungen. Überprüfen Sie Ihren API-Schlüssel in den Einstellungen oder den Early Access-Status.",
"pleaseSelectVersion": "Bitte wählen Sie eine Version aus",
"versionExists": "Diese Version existiert bereits in Ihrer Bibliothek",
"downloadCompleted": "Download erfolgreich abgeschlossen"
"downloadCompleted": "Download erfolgreich abgeschlossen",
"autoOrganizeSuccess": "Automatische Organisation für {count} {type} erfolgreich abgeschlossen",
"autoOrganizePartialSuccess": "Automatische Organisation abgeschlossen: {success} verschoben, {failures} fehlgeschlagen von insgesamt {total} Modellen",
"autoOrganizeFailed": "Automatische Organisation fehlgeschlagen: {error}",
"noModelsSelected": "Keine Modelle ausgewählt"
},
"recipes": {
"fetchFailed": "Fehler beim Abrufen der Rezepte: {message}",

View File

@@ -324,8 +324,18 @@
"copyAll": "Copy All Syntax",
"refreshAll": "Refresh All Metadata",
"moveAll": "Move All to Folder",
"autoOrganize": "Auto-Organize Selected",
"deleteAll": "Delete All Models",
"clear": "Clear Selection"
"clear": "Clear Selection",
"autoOrganizeProgress": {
"initializing": "Initializing auto-organize...",
"starting": "Starting auto-organize for {type}...",
"processing": "Processing ({processed}/{total}) - {success} moved, {skipped} skipped, {failures} failed",
"cleaning": "Cleaning up empty directories...",
"completed": "Completed: {success} moved, {skipped} skipped, {failures} failed",
"complete": "Auto-organize complete",
"error": "Error: {error}"
}
},
"contextMenu": {
"refreshMetadata": "Refresh Civitai Data",
@@ -942,7 +952,11 @@
"downloadPartialWithAccess": "Downloaded {completed} of {total} LoRAs. {accessFailures} failed due to access restrictions. Check your API key in settings or early access status.",
"pleaseSelectVersion": "Please select a version",
"versionExists": "This version already exists in your library",
"downloadCompleted": "Download completed successfully"
"downloadCompleted": "Download completed successfully",
"autoOrganizeSuccess": "Auto-organize completed successfully for {count} {type}",
"autoOrganizePartialSuccess": "Auto-organize completed with {success} moved, {failures} failed out of {total} models",
"autoOrganizeFailed": "Auto-organize failed: {error}",
"noModelsSelected": "No models selected"
},
"recipes": {
"fetchFailed": "Failed to fetch recipes: {message}",

View File

@@ -324,8 +324,18 @@
"copyAll": "Copiar toda la sintaxis",
"refreshAll": "Actualizar todos los metadatos",
"moveAll": "Mover todos a carpeta",
"autoOrganize": "Auto-organizar seleccionados",
"deleteAll": "Eliminar todos los modelos",
"clear": "Limpiar selección"
"clear": "Limpiar selección",
"autoOrganizeProgress": {
"initializing": "Inicializando auto-organización...",
"starting": "Iniciando auto-organización para {type}...",
"processing": "Procesando ({processed}/{total}) - {success} movidos, {skipped} omitidos, {failures} fallidos",
"cleaning": "Limpiando directorios vacíos...",
"completed": "Completado: {success} movidos, {skipped} omitidos, {failures} fallidos",
"complete": "Auto-organización completada",
"error": "Error: {error}"
}
},
"contextMenu": {
"refreshMetadata": "Actualizar datos de Civitai",
@@ -942,7 +952,11 @@
"downloadPartialWithAccess": "Descargados {completed} de {total} LoRAs. {accessFailures} fallaron debido a restricciones de acceso. Revisa tu clave API en configuración o estado de acceso temprano.",
"pleaseSelectVersion": "Por favor selecciona una versión",
"versionExists": "Esta versión ya existe en tu biblioteca",
"downloadCompleted": "Descarga completada exitosamente"
"downloadCompleted": "Descarga completada exitosamente",
"autoOrganizeSuccess": "Auto-organización completada exitosamente para {count} {type}",
"autoOrganizePartialSuccess": "Auto-organización completada con {success} movidos, {failures} fallidos de un total de {total} modelos",
"autoOrganizeFailed": "Auto-organización fallida: {error}",
"noModelsSelected": "No hay modelos seleccionados"
},
"recipes": {
"fetchFailed": "Error al obtener recetas: {message}",

View File

@@ -324,8 +324,18 @@
"copyAll": "Copier toute la syntaxe",
"refreshAll": "Actualiser toutes les métadonnées",
"moveAll": "Déplacer tout vers un dossier",
"autoOrganize": "Auto-organiser la sélection",
"deleteAll": "Supprimer tous les modèles",
"clear": "Effacer la sélection"
"clear": "Effacer la sélection",
"autoOrganizeProgress": {
"initializing": "Initialisation de l'auto-organisation...",
"starting": "Démarrage de l'auto-organisation pour {type}...",
"processing": "Traitement ({processed}/{total}) - {success} déplacés, {skipped} ignorés, {failures} échecs",
"cleaning": "Nettoyage des répertoires vides...",
"completed": "Terminé : {success} déplacés, {skipped} ignorés, {failures} échecs",
"complete": "Auto-organisation terminée",
"error": "Erreur : {error}"
}
},
"contextMenu": {
"refreshMetadata": "Actualiser les données Civitai",
@@ -942,7 +952,11 @@
"downloadPartialWithAccess": "{completed} sur {total} LoRAs téléchargés. {accessFailures} ont échoué en raison de restrictions d'accès. Vérifiez votre clé API dans les paramètres ou le statut d'accès anticipé.",
"pleaseSelectVersion": "Veuillez sélectionner une version",
"versionExists": "Cette version existe déjà dans votre bibliothèque",
"downloadCompleted": "Téléchargement terminé avec succès"
"downloadCompleted": "Téléchargement terminé avec succès",
"autoOrganizeSuccess": "Auto-organisation terminée avec succès pour {count} {type}",
"autoOrganizePartialSuccess": "Auto-organisation terminée avec {success} déplacés, {failures} échecs sur {total} modèles",
"autoOrganizeFailed": "Échec de l'auto-organisation : {error}",
"noModelsSelected": "Aucun modèle sélectionné"
},
"recipes": {
"fetchFailed": "Échec de la récupération des recipes : {message}",

View File

@@ -324,8 +324,18 @@
"copyAll": "すべての構文をコピー",
"refreshAll": "すべてのメタデータを更新",
"moveAll": "すべてをフォルダに移動",
"autoOrganize": "自動整理を実行",
"deleteAll": "すべてのモデルを削除",
"clear": "選択をクリア"
"clear": "選択をクリア",
"autoOrganizeProgress": {
"initializing": "自動整理を初期化中...",
"starting": "{type}の自動整理を開始中...",
"processing": "処理中({processed}/{total}- {success} 移動、{skipped} スキップ、{failures} 失敗",
"cleaning": "空のディレクトリをクリーンアップ中...",
"completed": "完了:{success} 移動、{skipped} スキップ、{failures} 失敗",
"complete": "自動整理が完了しました",
"error": "エラー:{error}"
}
},
"contextMenu": {
"refreshMetadata": "Civitaiデータを更新",
@@ -942,7 +952,11 @@
"downloadPartialWithAccess": "{total} LoRAのうち {completed} がダウンロードされました。{accessFailures} はアクセス制限により失敗しました。設定でAPIキーまたはアーリーアクセス状況を確認してください。",
"pleaseSelectVersion": "バージョンを選択してください",
"versionExists": "このバージョンは既にライブラリに存在します",
"downloadCompleted": "ダウンロードが正常に完了しました"
"downloadCompleted": "ダウンロードが正常に完了しました",
"autoOrganizeSuccess": "{count} {type} の自動整理が正常に完了しました",
"autoOrganizePartialSuccess": "自動整理が完了しました:{total} モデル中 {success} 移動、{failures} 失敗",
"autoOrganizeFailed": "自動整理に失敗しました:{error}",
"noModelsSelected": "モデルが選択されていません"
},
"recipes": {
"fetchFailed": "レシピの取得に失敗しました:{message}",

View File

@@ -324,8 +324,18 @@
"copyAll": "모든 문법 복사",
"refreshAll": "모든 메타데이터 새로고침",
"moveAll": "모두 폴더로 이동",
"autoOrganize": "자동 정리 선택",
"deleteAll": "모든 모델 삭제",
"clear": "선택 지우기"
"clear": "선택 지우기",
"autoOrganizeProgress": {
"initializing": "자동 정리 초기화 중...",
"starting": "{type}에 대한 자동 정리 시작...",
"processing": "처리 중 ({processed}/{total}) - {success}개 이동, {skipped}개 건너뜀, {failures}개 실패",
"cleaning": "빈 디렉토리 정리 중...",
"completed": "완료: {success}개 이동, {skipped}개 건너뜀, {failures}개 실패",
"complete": "자동 정리 완료",
"error": "오류: {error}"
}
},
"contextMenu": {
"refreshMetadata": "Civitai 데이터 새로고침",
@@ -942,7 +952,11 @@
"downloadPartialWithAccess": "{total}개 중 {completed}개 LoRA가 다운로드되었습니다. {accessFailures}개는 액세스 제한으로 실패했습니다. 설정에서 API 키 또는 얼리 액세스 상태를 확인하세요.",
"pleaseSelectVersion": "버전을 선택해주세요",
"versionExists": "이 버전은 이미 라이브러리에 있습니다",
"downloadCompleted": "다운로드가 성공적으로 완료되었습니다"
"downloadCompleted": "다운로드가 성공적으로 완료되었습니다",
"autoOrganizeSuccess": "{count}개의 {type}에 대해 자동 정리가 성공적으로 완료되었습니다",
"autoOrganizePartialSuccess": "자동 정리 완료: 전체 {total}개 중 {success}개 이동, {failures}개 실패",
"autoOrganizeFailed": "자동 정리 실패: {error}",
"noModelsSelected": "선택된 모델이 없습니다"
},
"recipes": {
"fetchFailed": "레시피 가져오기 실패: {message}",

View File

@@ -324,8 +324,18 @@
"copyAll": "Копировать весь синтаксис",
"refreshAll": "Обновить все метаданные",
"moveAll": "Переместить все в папку",
"autoOrganize": "Автоматически организовать выбранные",
"deleteAll": "Удалить все модели",
"clear": "Очистить выбор"
"clear": "Очистить выбор",
"autoOrganizeProgress": {
"initializing": "Инициализация автоматической организации...",
"starting": "Запуск автоматической организации для {type}...",
"processing": "Обработка ({processed}/{total}) — {success} перемещено, {skipped} пропущено, {failures} не удалось",
"cleaning": "Очистка пустых директорий...",
"completed": "Завершено: {success} перемещено, {skipped} пропущено, {failures} не удалось",
"complete": "Автоматическая организация завершена",
"error": "Ошибка: {error}"
}
},
"contextMenu": {
"refreshMetadata": "Обновить данные Civitai",
@@ -942,7 +952,11 @@
"downloadPartialWithAccess": "Загружено {completed} из {total} LoRAs. {accessFailures} не удалось из-за ограничений доступа. Проверьте ваш API ключ в настройках или статус раннего доступа.",
"pleaseSelectVersion": "Пожалуйста, выберите версию",
"versionExists": "Эта версия уже существует в вашей библиотеке",
"downloadCompleted": "Загрузка успешно завершена"
"downloadCompleted": "Загрузка успешно завершена",
"autoOrganizeSuccess": "Автоматическая организация успешно завершена для {count} {type}",
"autoOrganizePartialSuccess": "Автоматическая организация завершена: перемещено {success}, не удалось {failures} из {total} моделей",
"autoOrganizeFailed": "Ошибка автоматической организации: {error}",
"noModelsSelected": "Модели не выбраны"
},
"recipes": {
"fetchFailed": "Не удалось получить рецепты: {message}",

View File

@@ -324,8 +324,18 @@
"copyAll": "复制全部语法",
"refreshAll": "刷新全部元数据",
"moveAll": "全部移动到文件夹",
"autoOrganize": "自动整理所选模型",
"deleteAll": "删除所有模型",
"clear": "清除选择"
"clear": "清除选择",
"autoOrganizeProgress": {
"initializing": "正在初始化自动整理...",
"starting": "正在为 {type} 启动自动整理...",
"processing": "处理中({processed}/{total}- 已移动 {success} 个,跳过 {skipped} 个,失败 {failures} 个",
"cleaning": "正在清理空文件夹...",
"completed": "完成:已移动 {success} 个,跳过 {skipped} 个,失败 {failures} 个",
"complete": "自动整理已完成",
"error": "错误:{error}"
}
},
"contextMenu": {
"refreshMetadata": "刷新 Civitai 数据",
@@ -942,7 +952,11 @@
"downloadPartialWithAccess": "已下载 {completed}/{total} 个 LoRA。{accessFailures} 个因访问限制失败。请检查设置中的 API 密钥或早期访问状态。",
"pleaseSelectVersion": "请选择版本",
"versionExists": "该版本已存在于你的库中",
"downloadCompleted": "下载成功完成"
"downloadCompleted": "下载成功完成",
"autoOrganizeSuccess": "自动整理已成功完成,共 {count} 个 {type}",
"autoOrganizePartialSuccess": "自动整理完成:已移动 {success} 个,{failures} 个失败,共 {total} 个模型",
"autoOrganizeFailed": "自动整理失败:{error}",
"noModelsSelected": "未选中模型"
},
"recipes": {
"fetchFailed": "获取配方失败:{message}",

View File

@@ -324,8 +324,18 @@
"copyAll": "複製全部語法",
"refreshAll": "刷新全部 metadata",
"moveAll": "全部移動到資料夾",
"autoOrganize": "自動整理所選模型",
"deleteAll": "刪除全部模型",
"clear": "清除選取"
"clear": "清除選取",
"autoOrganizeProgress": {
"initializing": "正在初始化自動整理...",
"starting": "正在開始自動整理 {type}...",
"processing": "處理中({processed}/{total}- 已移動 {success},已略過 {skipped},失敗 {failures}",
"cleaning": "正在清理空資料夾...",
"completed": "完成:已移動 {success},已略過 {skipped},失敗 {failures}",
"complete": "自動整理完成",
"error": "錯誤:{error}"
}
},
"contextMenu": {
"refreshMetadata": "刷新 Civitai 資料",
@@ -942,7 +952,11 @@
"downloadPartialWithAccess": "已下載 {completed} 個 LoRA共 {total} 個。{accessFailures} 個因訪問限制而失敗。請檢查您的 API 密鑰或提前訪問狀態。",
"pleaseSelectVersion": "請選擇一個版本",
"versionExists": "此版本已存在於您的庫中",
"downloadCompleted": "下載成功完成"
"downloadCompleted": "下載成功完成",
"autoOrganizeSuccess": "自動整理已成功完成,共 {count} 個 {type} 已整理",
"autoOrganizePartialSuccess": "自動整理完成:已移動 {success} 個,{failures} 個失敗,共 {total} 個模型",
"autoOrganizeFailed": "自動整理失敗:{error}",
"noModelsSelected": "未選擇任何模型"
},
"recipes": {
"fetchFailed": "取得配方失敗:{message}",

View File

@@ -56,6 +56,7 @@ class BaseModelRoutes(ABC):
app.router.add_post(f'/api/{prefix}/move_model', self.move_model)
app.router.add_post(f'/api/{prefix}/move_models_bulk', self.move_models_bulk)
app.router.add_get(f'/api/{prefix}/auto-organize', self.auto_organize_models)
app.router.add_post(f'/api/{prefix}/auto-organize', self.auto_organize_models)
app.router.add_get(f'/api/{prefix}/auto-organize-progress', self.get_auto_organize_progress)
# Common query routes
@@ -773,7 +774,7 @@ class BaseModelRoutes(ABC):
return web.Response(text=str(e), status=500)
async def auto_organize_models(self, request: web.Request) -> web.Response:
"""Auto-organize all models based on current settings"""
"""Auto-organize all models or a specific set of models based on current settings"""
try:
# Check if auto-organize is already running
if ws_manager.is_auto_organize_running():
@@ -791,8 +792,17 @@ class BaseModelRoutes(ABC):
'error': 'Auto-organize is already running. Please wait for it to complete.'
}, status=409)
# Get specific file paths from request if this is a POST with selected models
file_paths = None
if request.method == 'POST':
try:
data = await request.json()
file_paths = data.get('file_paths')
except Exception:
pass # Continue with all models if no valid JSON
async with auto_organize_lock:
return await self._perform_auto_organize()
return await self._perform_auto_organize(file_paths)
except Exception as e:
logger.error(f"Error in auto_organize_models: {e}", exc_info=True)
@@ -809,20 +819,33 @@ class BaseModelRoutes(ABC):
'error': str(e)
}, status=500)
async def _perform_auto_organize(self) -> web.Response:
"""Perform the actual auto-organize operation"""
async def _perform_auto_organize(self, file_paths=None) -> web.Response:
"""Perform the actual auto-organize operation
Args:
file_paths: Optional list of specific file paths to organize.
If None, organizes all models.
"""
try:
# Get all models from cache
cache = await self.service.scanner.get_cached_data()
all_models = cache.raw_data
# Filter models if specific file paths are provided
if file_paths:
all_models = [model for model in all_models if model.get('file_path') in file_paths]
operation_type = 'bulk'
else:
operation_type = 'all'
# Get model roots for this scanner
model_roots = self.service.get_model_roots()
if not model_roots:
await ws_manager.broadcast_auto_organize_progress({
'type': 'auto_organize_progress',
'status': 'error',
'error': 'No model roots configured'
'error': 'No model roots configured',
'operation_type': operation_type
})
return web.json_response({
'success': False,
@@ -849,7 +872,8 @@ class BaseModelRoutes(ABC):
'processed': 0,
'success': 0,
'failures': 0,
'skipped': 0
'skipped': 0,
'operation_type': operation_type
})
# Process models in batches
@@ -980,7 +1004,8 @@ class BaseModelRoutes(ABC):
'processed': processed,
'success': success_count,
'failures': failure_count,
'skipped': skipped_count
'skipped': skipped_count,
'operation_type': operation_type
})
# Small delay between batches to prevent overwhelming the system
@@ -995,7 +1020,8 @@ class BaseModelRoutes(ABC):
'success': success_count,
'failures': failure_count,
'skipped': skipped_count,
'message': 'Cleaning up empty directories...'
'message': 'Cleaning up empty directories...',
'operation_type': operation_type
})
# Clean up empty directories after organizing
@@ -1014,20 +1040,22 @@ class BaseModelRoutes(ABC):
'success': success_count,
'failures': failure_count,
'skipped': skipped_count,
'cleanup': cleanup_counts
'cleanup': cleanup_counts,
'operation_type': operation_type
})
# Prepare response with limited details
response_data = {
'success': True,
'message': f'Auto-organize completed: {success_count} moved, {skipped_count} skipped, {failure_count} failed out of {total_models} total',
'message': f'Auto-organize {operation_type} completed: {success_count} moved, {skipped_count} skipped, {failure_count} failed out of {total_models} total',
'summary': {
'total': total_models,
'success': success_count,
'skipped': skipped_count,
'failures': failure_count,
'organization_type': 'flat' if is_flat_structure else 'structured',
'cleaned_dirs': cleanup_counts
'cleaned_dirs': cleanup_counts,
'operation_type': operation_type
}
}
@@ -1047,7 +1075,8 @@ class BaseModelRoutes(ABC):
await ws_manager.broadcast_auto_organize_progress({
'type': 'auto_organize_progress',
'status': 'error',
'error': str(e)
'error': str(e),
'operation_type': operation_type if 'operation_type' in locals() else 'unknown'
})
raise e

View File

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

View File

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

View File

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

View File

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

View File

@@ -71,6 +71,9 @@
<div class="context-menu-item" data-action="move-all">
<i class="fas fa-folder-open"></i> <span>{{ t('loras.bulkOperations.moveAll') }}</span>
</div>
<div class="context-menu-item" data-action="auto-organize">
<i class="fas fa-magic"></i> <span>{{ t('loras.bulkOperations.autoOrganize') }}</span>
</div>
<div class="context-menu-separator"></div>
<div class="context-menu-item delete-item" data-action="delete-all">
<i class="fas fa-trash"></i> <span>{{ t('loras.bulkOperations.deleteAll') }}</span>