From 1044fa3c83b7b4fc9b7dca5d3bd06e63bf0702bd Mon Sep 17 00:00:00 2001 From: Will Miao Date: Mon, 25 May 2026 21:25:35 +0800 Subject: [PATCH] feat(doctor): improve duplicate filename conflict UX with confirm modal, syntax-format nav, and i18n - Remove [LoRAs] prefix noise from conflict detail display - Limit inline conflict groups to 5, show remainder count - Add 'Switch to Full Path Syntax' action in conflict card - Add confirmation modal before resolving conflicts (shows rename strategy) - Register resolveFilenameConflictsModal in ModalManager (fix no-op showModal) - Switch to Interface section and add highlight animation on syntax-format nav - Sync and translate conflictConfirm strings across all 10 locales --- locales/de.json | 7 ++ locales/en.json | 7 ++ locales/es.json | 7 ++ locales/fr.json | 7 ++ locales/he.json | 7 ++ locales/ja.json | 7 ++ locales/ko.json | 7 ++ locales/ru.json | 7 ++ locales/zh-CN.json | 7 ++ locales/zh-TW.json | 7 ++ py/routes/handlers/misc_handlers.py | 20 +++- static/css/components/modal/delete-modal.css | 33 +++++++ .../css/components/modal/settings-modal.css | 11 +++ static/js/managers/DoctorManager.js | 94 ++++++++++++++++++- static/js/managers/ModalManager.js | 16 +++- .../components/modals/confirm_modals.html | 17 ++++ 16 files changed, 256 insertions(+), 5 deletions(-) diff --git a/locales/de.json b/locales/de.json index 030d92e7..c7c73326 100644 --- a/locales/de.json +++ b/locales/de.json @@ -1941,6 +1941,13 @@ "conflictsResolveFailed": "Auflösung der Dateinamenskonflikte fehlgeschlagen: {message}" } }, + "conflictConfirm": { + "title": "Dateinamenskonflikte auflösen", + "message": "Umbenennen durch Anhängen eines 4-stelligen Hashs an jeden doppelten Dateinamen.", + "note": "Dieser Vorgang benennt Dateien auf der Festplatte um. Modellreferenzen in vorhandenen Workflows müssen möglicherweise aktualisiert werden, wenn Sie das A1111-Syntaxformat verwenden.", + "confirm": "Dateien umbenennen", + "cancel": "Abbrechen" + }, "banners": { "versionMismatch": { "title": "Anwendungs-Update erkannt", diff --git a/locales/en.json b/locales/en.json index df07ed78..d241cebe 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1941,6 +1941,13 @@ "conflictsResolveFailed": "Failed to resolve filename conflicts: {message}" } }, + "conflictConfirm": { + "title": "Resolve Filename Conflicts", + "message": "Renaming by appending a 4-character hash to each duplicate filename.", + "note": "This operation renames files on disk. Model references in existing workflows may need updating if you use the A1111 syntax format.", + "confirm": "Rename Files", + "cancel": "Cancel" + }, "banners": { "versionMismatch": { "title": "Application Update Detected", diff --git a/locales/es.json b/locales/es.json index 4ec1c110..e8375dfe 100644 --- a/locales/es.json +++ b/locales/es.json @@ -1941,6 +1941,13 @@ "conflictsResolveFailed": "Error al resolver conflictos de nombre de archivo: {message}" } }, + "conflictConfirm": { + "title": "Resolver conflictos de nombres de archivo", + "message": "Renombrar añadiendo un hash de 4 caracteres a cada nombre de archivo duplicado.", + "note": "Esta operación renombra archivos en el disco. Es posible que las referencias a modelos en flujos de trabajo existentes deban actualizarse si usas el formato de sintaxis A1111.", + "confirm": "Renombrar archivos", + "cancel": "Cancelar" + }, "banners": { "versionMismatch": { "title": "Actualización de la aplicación detectada", diff --git a/locales/fr.json b/locales/fr.json index 72ce096c..e5c73852 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -1941,6 +1941,13 @@ "conflictsResolveFailed": "Échec de la résolution des conflits de nom de fichier : {message}" } }, + "conflictConfirm": { + "title": "Résoudre les conflits de noms de fichiers", + "message": "Renommer en ajoutant un hachage de 4 caractères à chaque nom de fichier en double.", + "note": "Cette opération renomme les fichiers sur le disque. Les références de modèle dans les workflows existants peuvent nécessiter une mise à jour si vous utilisez le format de syntaxe A1111.", + "confirm": "Renommer les fichiers", + "cancel": "Annuler" + }, "banners": { "versionMismatch": { "title": "Mise à jour de l'application détectée", diff --git a/locales/he.json b/locales/he.json index a4206f75..25560175 100644 --- a/locales/he.json +++ b/locales/he.json @@ -1941,6 +1941,13 @@ "conflictsResolveFailed": "פתרון התנגשויות שמות קבצים נכשל: {message}" } }, + "conflictConfirm": { + "title": "פתור התנגשויות בשמות קבצים", + "message": "שינוי שם על ידי הוספת האש באורך 4 תווים לכל שם קובץ כפול.", + "note": "פעולה זו משנה שמות של קבצים בדיסק. ייתכן שיהיה צורך לעדכן הפניות למודלים בזרימות עבודה קיימות אם אתה משתמש בפורמט התחביר A1111.", + "confirm": "שנה שמות קבצים", + "cancel": "ביטול" + }, "banners": { "versionMismatch": { "title": "זוהה עדכון יישום", diff --git a/locales/ja.json b/locales/ja.json index 4c76216f..1a182dd2 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -1941,6 +1941,13 @@ "conflictsResolveFailed": "ファイル名競合の解決に失敗しました: {message}" } }, + "conflictConfirm": { + "title": "ファイル名の競合を解決", + "message": "重複したファイル名に4文字のハッシュを追加してリネームします。", + "note": "この操作はディスク上のファイルをリネームします。A1111 構文形式を使用している場合、既存のワークフロー内のモデル参照を更新する必要があるかもしれません。", + "confirm": "ファイルをリネーム", + "cancel": "キャンセル" + }, "banners": { "versionMismatch": { "title": "アプリケーション更新が検出されました", diff --git a/locales/ko.json b/locales/ko.json index 903ea104..80eae705 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -1941,6 +1941,13 @@ "conflictsResolveFailed": "파일명 충돌 해결 실패: {message}" } }, + "conflictConfirm": { + "title": "파일명 충돌 해결", + "message": "중복 파일명에 4자리 해시를 추가하여 이름을 변경합니다.", + "note": "이 작업은 디스크에 있는 파일의 이름을 변경합니다. A1111 구문 형식을 사용하는 경우 기존 워크플로우의 모델 참조를 업데이트해야 할 수 있습니다.", + "confirm": "파일 이름 변경", + "cancel": "취소" + }, "banners": { "versionMismatch": { "title": "애플리케이션 업데이트 감지", diff --git a/locales/ru.json b/locales/ru.json index bddeca81..06d97d2c 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -1941,6 +1941,13 @@ "conflictsResolveFailed": "Не удалось разрешить конфликты имён файлов: {message}" } }, + "conflictConfirm": { + "title": "Разрешить конфликты имён файлов", + "message": "Переименование с добавлением 4-символьного хеша к каждому дублирующемуся имени файла.", + "note": "Эта операция переименовывает файлы на диске. Если вы используете синтаксис A1111, ссылки на модели в существующих рабочих процессах могут потребовать обновления.", + "confirm": "Переименовать файлы", + "cancel": "Отмена" + }, "banners": { "versionMismatch": { "title": "Обнаружено обновление приложения", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 11143c17..d0bee6b7 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -1941,6 +1941,13 @@ "conflictsResolveFailed": "解决文件名冲突失败:{message}" } }, + "conflictConfirm": { + "title": "解决文件名冲突", + "message": "通过在每个重复文件名后附加 4 位哈希值来重命名文件。", + "note": "此操作会重命名磁盘上的文件。如果使用 A1111 语法格式,现有工作流中的模型引用可能需要更新。", + "confirm": "重命名文件", + "cancel": "取消" + }, "banners": { "versionMismatch": { "title": "检测到应用更新", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 68c1cfbd..94856a2b 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -1941,6 +1941,13 @@ "conflictsResolveFailed": "解決檔案名稱衝突失敗:{message}" } }, + "conflictConfirm": { + "title": "解決檔案名稱衝突", + "message": "通過在每個重複檔案名稱後附加 4 位元哈希值來重新命名檔案。", + "note": "此操作會重新命名磁碟上的檔案。如果使用 A1111 語法格式,現有工作流程中的模型參考可能需要更新。", + "confirm": "重新命名檔案", + "cancel": "取消" + }, "banners": { "versionMismatch": { "title": "偵測到應用程式更新", diff --git a/py/routes/handlers/misc_handlers.py b/py/routes/handlers/misc_handlers.py index 14624a70..25fb5be5 100644 --- a/py/routes/handlers/misc_handlers.py +++ b/py/routes/handlers/misc_handlers.py @@ -1063,12 +1063,22 @@ class DoctorHandler: "total_conflict_files": total_conflict_files, } ] - for conflict in all_conflicts: + + # Show at most 5 conflict groups inline; note any remainder. + MAX_VISIBLE_CONFLICTS = 5 + visible_conflicts = all_conflicts[:MAX_VISIBLE_CONFLICTS] + for conflict in visible_conflicts: details.append( - f"[{conflict['label']}] '{conflict['filename']}' " + f"'{conflict['filename']}' " f"found in {len(conflict['paths'])} locations" ) + hidden_count = len(all_conflicts) - MAX_VISIBLE_CONFLICTS + if hidden_count > 0: + details.append( + f"...and {hidden_count} more duplicate filename group(s)" + ) + return { "id": "filename_conflicts", "title": "Duplicate Filename Conflicts", @@ -1079,7 +1089,11 @@ class DoctorHandler: { "id": "resolve-filename-conflicts", "label": "Resolve Conflicts", - } + }, + { + "id": "open-settings-syntax-format", + "label": "Switch to Full Path Syntax", + }, ], } diff --git a/static/css/components/modal/delete-modal.css b/static/css/components/modal/delete-modal.css index 7a1334a4..68e32f82 100644 --- a/static/css/components/modal/delete-modal.css +++ b/static/css/components/modal/delete-modal.css @@ -33,6 +33,39 @@ animation: modalFadeIn 0.2s ease-out; } +#resolveFilenameConflictsModal .confirmation-message { + color: var(--text-color); + margin: var(--space-2) 0; + font-size: 1em; + line-height: 1.5; +} + +#resolveFilenameConflictsModal .resolve-conflicts-detail { + color: var(--text-color); + margin: var(--space-2) 0; + font-size: 0.95em; + line-height: 1.5; +} + +#resolveFilenameConflictsModal .resolve-conflicts-detail code { + background: var(--lora-surface); + padding: 2px 6px; + border-radius: 3px; + font-family: monospace; + border: 1px solid var(--lora-border); +} + +#resolveFilenameConflictsModal .resolve-conflicts-impact { + background: var(--lora-surface); + border: 1px solid var(--lora-border); + border-radius: var(--border-radius-sm); + padding: var(--space-2); + margin: var(--space-2) 0; + color: var(--text-color); + text-align: left; + line-height: 1.5; +} + .delete-model-info, .exclude-model-info { /* Update info display styling */ diff --git a/static/css/components/modal/settings-modal.css b/static/css/components/modal/settings-modal.css index a0f960b0..66fc53a6 100644 --- a/static/css/components/modal/settings-modal.css +++ b/static/css/components/modal/settings-modal.css @@ -1369,3 +1369,14 @@ input:checked + .toggle-slider:before { background: var(--lora-error); color: white; } + +/* Highlight animation for setting items targeted from Doctor actions */ +@keyframes settings-highlight-pulse { + 0%, 100% { box-shadow: 0 0 0 0 rgba(from var(--lora-accent) r g b / 0.4); } + 50% { box-shadow: 0 0 0 4px rgba(from var(--lora-accent) r g b / 0.2); } +} + +.settings-setting-highlight { + animation: settings-highlight-pulse 1.5s ease-in-out 3; + border-radius: var(--border-radius-xs); +} diff --git a/static/js/managers/DoctorManager.js b/static/js/managers/DoctorManager.js index 0aaefd2b..fbb365c9 100644 --- a/static/js/managers/DoctorManager.js +++ b/static/js/managers/DoctorManager.js @@ -324,11 +324,42 @@ export class DoctorManager { } }, 100); break; + case 'open-settings-syntax-format': + modalManager.showModal('settingsModal'); + window.setTimeout(() => { + // Switch to Interface section + document.querySelectorAll('.settings-section').forEach((s) => s.classList.remove('active')); + const interfaceSection = document.getElementById('section-interface'); + if (interfaceSection) { + interfaceSection.classList.add('active'); + } + document.querySelectorAll('.settings-nav-item').forEach((n) => n.classList.remove('active')); + const interfaceNav = document.querySelector('.settings-nav-item[data-section="interface"]'); + if (interfaceNav) { + interfaceNav.classList.add('active'); + } + + // Focus and scroll to the LoRA Syntax Format dropdown + const select = document.getElementById('loraSyntaxFormat'); + if (select) { + select.focus(); + select.scrollIntoView({ behavior: 'smooth', block: 'center' }); + // Add temporary highlight animation + const settingItem = select.closest('.setting-item'); + if (settingItem) { + settingItem.classList.add('settings-setting-highlight'); + setTimeout(() => { + settingItem.classList.remove('settings-setting-highlight'); + }, 4500); + } + } + }, 100); + break; case 'repair-cache': await this.repairCache(); break; case 'resolve-filename-conflicts': - await this.resolveFilenameConflicts(); + await this.promptResolveConflicts(); break; case 'reload-page': this.reloadUi(); @@ -358,6 +389,62 @@ export class DoctorManager { } } + _getConflictStats() { + const conflict = (this.lastDiagnostics?.diagnostics || []).find( + (d) => d.id === 'filename_conflicts' + ); + if (!conflict || !Array.isArray(conflict.details)) { + return { groups: 0, files: 0 }; + } + const summary = conflict.details.find( + (d) => d && typeof d === 'object' && d.conflict_groups !== undefined + ); + return { + groups: summary?.conflict_groups || 0, + files: summary?.total_conflict_files || 0, + }; + } + + async promptResolveConflicts() { + const stats = this._getConflictStats(); + if (stats.groups === 0) { + return; + } + + const detailEl = document.getElementById('resolveConflictsDetail'); + if (detailEl) { + detailEl.innerHTML = translate( + 'conflictConfirm.detail', + {}, + 'Example: Add_Details_v1.2 \u2192 Add_Details_v1.2-a3f7' + ); + } + + const impactEl = document.getElementById('resolveConflictsImpact'); + if (impactEl) { + impactEl.innerHTML = translate( + 'conflictConfirm.impact', + { count: stats.files, groups: stats.groups }, + `Will rename ${stats.files} file(s) across ${stats.groups} duplicate group(s).` + ); + } + + this._confirmResolveResolve = null; + modalManager.showModal('resolveFilenameConflictsModal'); + return new Promise((resolve) => { + this._confirmResolveResolve = resolve; + }); + } + + async confirmResolveConflicts() { + modalManager.closeModal('resolveFilenameConflictsModal'); + if (this._confirmResolveResolve) { + this._confirmResolveResolve(true); + this._confirmResolveResolve = null; + } + await this.resolveFilenameConflicts(); + } + async resolveFilenameConflicts() { try { this.setLoading(true); @@ -449,3 +536,8 @@ export class DoctorManager { } export const doctorManager = new DoctorManager(); + +// Make available globally for HTML onclick handlers +if (typeof window !== 'undefined') { + window.doctorManager = doctorManager; +} diff --git a/static/js/managers/ModalManager.js b/static/js/managers/ModalManager.js index c4653655..414fe682 100644 --- a/static/js/managers/ModalManager.js +++ b/static/js/managers/ModalManager.js @@ -316,6 +316,19 @@ export class ModalManager { }); } + // Register resolveFilenameConflictsModal + const resolveFilenameConflictsModal = document.getElementById('resolveFilenameConflictsModal'); + if (resolveFilenameConflictsModal) { + this.registerModal('resolveFilenameConflictsModal', { + element: resolveFilenameConflictsModal, + onClose: () => { + this.getModal('resolveFilenameConflictsModal').element.classList.remove('show'); + document.body.classList.remove('modal-open'); + }, + closeOnOutsideClick: true + }); + } + document.addEventListener('keydown', this.boundHandleEscape); this.initialized = true; } @@ -396,7 +409,8 @@ export class ModalManager { id === "modelDuplicateDeleteModal" || id === "clearCacheModal" || id === "bulkDeleteModal" || - id === "checkUpdatesConfirmModal" + id === "checkUpdatesConfirmModal" || + id === "resolveFilenameConflictsModal" ) { modal.element.classList.add("show"); } else { diff --git a/templates/components/modals/confirm_modals.html b/templates/components/modals/confirm_modals.html index 6c4fedea..d7440885 100644 --- a/templates/components/modals/confirm_modals.html +++ b/templates/components/modals/confirm_modals.html @@ -108,4 +108,21 @@ + + + + \ No newline at end of file