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
This commit is contained in:
Will Miao
2026-05-25 21:25:35 +08:00
parent 397892bb7f
commit 1044fa3c83
16 changed files with 256 additions and 5 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -1941,6 +1941,13 @@
"conflictsResolveFailed": "פתרון התנגשויות שמות קבצים נכשל: {message}"
}
},
"conflictConfirm": {
"title": "פתור התנגשויות בשמות קבצים",
"message": "שינוי שם על ידי הוספת האש באורך 4 תווים לכל שם קובץ כפול.",
"note": "פעולה זו משנה שמות של קבצים בדיסק. ייתכן שיהיה צורך לעדכן הפניות למודלים בזרימות עבודה קיימות אם אתה משתמש בפורמט התחביר A1111.",
"confirm": "שנה שמות קבצים",
"cancel": "ביטול"
},
"banners": {
"versionMismatch": {
"title": "זוהה עדכון יישום",

View File

@@ -1941,6 +1941,13 @@
"conflictsResolveFailed": "ファイル名競合の解決に失敗しました: {message}"
}
},
"conflictConfirm": {
"title": "ファイル名の競合を解決",
"message": "重複したファイル名に4文字のハッシュを追加してリネームします。",
"note": "この操作はディスク上のファイルをリネームします。A1111 構文形式を使用している場合、既存のワークフロー内のモデル参照を更新する必要があるかもしれません。",
"confirm": "ファイルをリネーム",
"cancel": "キャンセル"
},
"banners": {
"versionMismatch": {
"title": "アプリケーション更新が検出されました",

View File

@@ -1941,6 +1941,13 @@
"conflictsResolveFailed": "파일명 충돌 해결 실패: {message}"
}
},
"conflictConfirm": {
"title": "파일명 충돌 해결",
"message": "중복 파일명에 4자리 해시를 추가하여 이름을 변경합니다.",
"note": "이 작업은 디스크에 있는 파일의 이름을 변경합니다. A1111 구문 형식을 사용하는 경우 기존 워크플로우의 모델 참조를 업데이트해야 할 수 있습니다.",
"confirm": "파일 이름 변경",
"cancel": "취소"
},
"banners": {
"versionMismatch": {
"title": "애플리케이션 업데이트 감지",

View File

@@ -1941,6 +1941,13 @@
"conflictsResolveFailed": "Не удалось разрешить конфликты имён файлов: {message}"
}
},
"conflictConfirm": {
"title": "Разрешить конфликты имён файлов",
"message": "Переименование с добавлением 4-символьного хеша к каждому дублирующемуся имени файла.",
"note": "Эта операция переименовывает файлы на диске. Если вы используете синтаксис A1111, ссылки на модели в существующих рабочих процессах могут потребовать обновления.",
"confirm": "Переименовать файлы",
"cancel": "Отмена"
},
"banners": {
"versionMismatch": {
"title": "Обнаружено обновление приложения",

View File

@@ -1941,6 +1941,13 @@
"conflictsResolveFailed": "解决文件名冲突失败:{message}"
}
},
"conflictConfirm": {
"title": "解决文件名冲突",
"message": "通过在每个重复文件名后附加 4 位哈希值来重命名文件。",
"note": "此操作会重命名磁盘上的文件。如果使用 A1111 语法格式,现有工作流中的模型引用可能需要更新。",
"confirm": "重命名文件",
"cancel": "取消"
},
"banners": {
"versionMismatch": {
"title": "检测到应用更新",

View File

@@ -1941,6 +1941,13 @@
"conflictsResolveFailed": "解決檔案名稱衝突失敗:{message}"
}
},
"conflictConfirm": {
"title": "解決檔案名稱衝突",
"message": "通過在每個重複檔案名稱後附加 4 位元哈希值來重新命名檔案。",
"note": "此操作會重新命名磁碟上的檔案。如果使用 A1111 語法格式,現有工作流程中的模型參考可能需要更新。",
"confirm": "重新命名檔案",
"cancel": "取消"
},
"banners": {
"versionMismatch": {
"title": "偵測到應用程式更新",

View File

@@ -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",
},
],
}

View File

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

View File

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

View File

@@ -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: <code>Add_Details_v1.2</code> \u2192 <code>Add_Details_v1.2-a3f7</code>'
);
}
const impactEl = document.getElementById('resolveConflictsImpact');
if (impactEl) {
impactEl.innerHTML = translate(
'conflictConfirm.impact',
{ count: stats.files, groups: stats.groups },
`Will rename <strong>${stats.files}</strong> file(s) across <strong>${stats.groups}</strong> 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;
}

View File

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

View File

@@ -108,4 +108,21 @@
</button>
</div>
</div>
</div>
<!-- Resolve Filename Conflicts Confirmation Modal -->
<div id="resolveFilenameConflictsModal" class="modal delete-modal">
<div class="modal-content delete-modal-content">
<h2>{{ t('conflictConfirm.title') }}</h2>
<p class="confirmation-message">{{ t('conflictConfirm.message') }}</p>
<p class="resolve-conflicts-detail" id="resolveConflictsDetail"></p>
<div class="resolve-conflicts-impact" id="resolveConflictsImpact"></div>
<div class="modal-actions">
<button class="cancel-btn" onclick="modalManager.closeModal('resolveFilenameConflictsModal')">{{ t('common.actions.cancel') }}</button>
<button class="primary-btn" id="resolveConflictsConfirmBtn" onclick="doctorManager.confirmResolveConflicts()">
<i class="fas fa-check"></i>
{{ t('conflictConfirm.confirm') }}
</button>
</div>
</div>
</div>