diff --git a/locales/de.json b/locales/de.json index 7ea7ffac..a9855674 100644 --- a/locales/de.json +++ b/locales/de.json @@ -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}", diff --git a/locales/en.json b/locales/en.json index b24c5770..093433c8 100644 --- a/locales/en.json +++ b/locales/en.json @@ -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}", diff --git a/locales/es.json b/locales/es.json index 9e86056c..274c497f 100644 --- a/locales/es.json +++ b/locales/es.json @@ -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}", diff --git a/locales/fr.json b/locales/fr.json index 0259c30e..a86b464c 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -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}", diff --git a/locales/ja.json b/locales/ja.json index 7fd5c068..cef7074b 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -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}", diff --git a/locales/ko.json b/locales/ko.json index ca707a3e..09765802 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -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}", diff --git a/locales/ru.json b/locales/ru.json index 071a80cb..c32f9357 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -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}", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 306e45e4..a9b6a1e6 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -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}", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 5752e13f..6c498946 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -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}", diff --git a/py/routes/base_model_routes.py b/py/routes/base_model_routes.py index 81f8edcd..099c2f58 100644 --- a/py/routes/base_model_routes.py +++ b/py/routes/base_model_routes.py @@ -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 diff --git a/static/js/api/apiConfig.js b/static/js/api/apiConfig.js index 44174119..33622dc5 100644 --- a/static/js/api/apiConfig.js +++ b/static/js/api/apiConfig.js @@ -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: {} }; diff --git a/static/js/api/baseModelApi.js b/static/js/api/baseModelApi.js index 485b4447..f4f2f16e 100644 --- a/static/js/api/baseModelApi.js +++ b/static/js/api/baseModelApi.js @@ -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') + }); + } } \ No newline at end of file diff --git a/static/js/components/ContextMenu/BulkContextMenu.js b/static/js/components/ContextMenu/BulkContextMenu.js index 7c0b42c9..638b803f 100644 --- a/static/js/components/ContextMenu/BulkContextMenu.js +++ b/static/js/components/ContextMenu/BulkContextMenu.js @@ -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; diff --git a/static/js/managers/BulkManager.js b/static/js/managers/BulkManager.js index ab58c33c..e3f67235 100644 --- a/static/js/managers/BulkManager.js +++ b/static/js/managers/BulkManager.js @@ -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'); } } diff --git a/templates/components/context_menu.html b/templates/components/context_menu.html index 52187615..b47ed57a 100644 --- a/templates/components/context_menu.html +++ b/templates/components/context_menu.html @@ -71,6 +71,9 @@
{{ t('loras.bulkOperations.moveAll') }}
+
+ {{ t('loras.bulkOperations.autoOrganize') }} +
{{ t('loras.bulkOperations.deleteAll') }}