diff --git a/locales/de.json b/locales/de.json index a67ecbb3..327dc004 100644 --- a/locales/de.json +++ b/locales/de.json @@ -903,6 +903,9 @@ "preparingDownload": "Download wird vorbereitet...", "reconnect": "Neu verknüpfen", "reconnectTooltip": "Mit einem lokalen LoRA neu verknüpfen", + "reconnectInstructions": "Geben Sie die LoRA-Syntax oder den Namen zum Neuverknüpfen ein:", + "reconnectExample": "Beispiel: oder nur der Name", + "reconnectPlaceholder": "LoRA-Namen oder -Syntax eingeben", "viewOnCivitai": "Auf CivitAI anzeigen", "openLoraDetails": "{name} in der LoRA-Bibliothek anzeigen", "openCheckpointDetails": "{name} in der Modellbibliothek anzeigen" diff --git a/locales/en.json b/locales/en.json index 1b9f5cf5..921ff549 100644 --- a/locales/en.json +++ b/locales/en.json @@ -903,6 +903,9 @@ "preparingDownload": "Preparing download...", "reconnect": "Reconnect", "reconnectTooltip": "Reconnect with a local LoRA", + "reconnectInstructions": "Enter LoRA syntax or name to reconnect:", + "reconnectExample": "Example: or just the name", + "reconnectPlaceholder": "Enter LoRA name or syntax", "viewOnCivitai": "View on CivitAI", "openLoraDetails": "View {name} in the LoRA library", "openCheckpointDetails": "View {name} in the model library" diff --git a/locales/es.json b/locales/es.json index efc5f668..fb4b18d7 100644 --- a/locales/es.json +++ b/locales/es.json @@ -903,6 +903,9 @@ "preparingDownload": "Preparando descarga...", "reconnect": "Reconectar", "reconnectTooltip": "Reconectar con un LoRA local", + "reconnectInstructions": "Introduce la sintaxis o el nombre del LoRA para reconectar:", + "reconnectExample": "Ejemplo: o solo el nombre", + "reconnectPlaceholder": "Introduce el nombre o la sintaxis del LoRA", "viewOnCivitai": "Ver en CivitAI", "openLoraDetails": "Ver {name} en la biblioteca de LoRAs", "openCheckpointDetails": "Ver {name} en la biblioteca de modelos" diff --git a/locales/fr.json b/locales/fr.json index 26de9099..ea5c9d93 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -903,6 +903,9 @@ "preparingDownload": "Préparation du téléchargement...", "reconnect": "Reconnecter", "reconnectTooltip": "Reconnecter avec un LoRA local", + "reconnectInstructions": "Entrez la syntaxe ou le nom du LoRA à reconnecter:", + "reconnectExample": "Exemple: ou simplement le nom", + "reconnectPlaceholder": "Entrez le nom ou la syntaxe du LoRA", "viewOnCivitai": "Voir sur CivitAI", "openLoraDetails": "Voir {name} dans la bibliothèque LoRA", "openCheckpointDetails": "Voir {name} dans la bibliothèque de modèles" diff --git a/locales/he.json b/locales/he.json index 51fc96a6..df1ef340 100644 --- a/locales/he.json +++ b/locales/he.json @@ -903,6 +903,9 @@ "preparingDownload": "מכין את ההורדה...", "reconnect": "חבר מחדש", "reconnectTooltip": "חבר מחדש עם LoRA מקומי", + "reconnectInstructions": "הזן תחביר או שם של LoRA לחיבור מחדש:", + "reconnectExample": "דוגמה: או רק את השם", + "reconnectPlaceholder": "הזן שם או תחביר של LoRA", "viewOnCivitai": "הצג ב-CivitAI", "openLoraDetails": "הצג את {name} בספריית ה-LoRA", "openCheckpointDetails": "הצג את {name} בספריית המודלים" diff --git a/locales/ja.json b/locales/ja.json index 04df3206..d6b46b0a 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -903,6 +903,9 @@ "preparingDownload": "ダウンロードを準備中...", "reconnect": "再接続", "reconnectTooltip": "ローカルの LoRA と再接続", + "reconnectInstructions": "再接続する LoRA の構文または名前を入力してください:", + "reconnectExample": "例: または名前のみ", + "reconnectPlaceholder": "LoRA 名または構文を入力", "viewOnCivitai": "CivitAI で表示", "openLoraDetails": "LoRA ライブラリで {name} を表示", "openCheckpointDetails": "モデルライブラリで {name} を表示" diff --git a/locales/ko.json b/locales/ko.json index 1b83fc85..1e8ddb2c 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -903,6 +903,9 @@ "preparingDownload": "다운로드 준비 중...", "reconnect": "다시 연결", "reconnectTooltip": "로컬 LoRA와 다시 연결", + "reconnectInstructions": "다시 연결할 LoRA 구문 또는 이름을 입력하세요:", + "reconnectExample": "예: 또는 이름만 입력", + "reconnectPlaceholder": "LoRA 이름 또는 구문 입력", "viewOnCivitai": "CivitAI에서 보기", "openLoraDetails": "LoRA 라이브러리에서 {name} 보기", "openCheckpointDetails": "모델 라이브러리에서 {name} 보기" diff --git a/locales/ru.json b/locales/ru.json index d6c13994..b3036449 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -903,6 +903,9 @@ "preparingDownload": "Подготовка к скачиванию...", "reconnect": "Переподключить", "reconnectTooltip": "Переподключить к локальному LoRA", + "reconnectInstructions": "Введите синтаксис или имя LoRA для переподключения:", + "reconnectExample": "Пример: или просто имя", + "reconnectPlaceholder": "Введите имя или синтаксис LoRA", "viewOnCivitai": "Открыть на CivitAI", "openLoraDetails": "Открыть {name} в библиотеке LoRA", "openCheckpointDetails": "Открыть {name} в библиотеке моделей" diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 867afc3b..557f6725 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -903,6 +903,9 @@ "preparingDownload": "正在准备下载...", "reconnect": "重新关联", "reconnectTooltip": "与本地 LoRA 重新关联", + "reconnectInstructions": "输入 LoRA 语法或名称以重新关联:", + "reconnectExample": "示例: 或只填名称", + "reconnectPlaceholder": "输入 LoRA 名称或语法", "viewOnCivitai": "在 CivitAI 上查看", "openLoraDetails": "在 LoRA 库中查看 {name}", "openCheckpointDetails": "在模型库中查看 {name}" diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 153a827a..6b7338d5 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -903,6 +903,9 @@ "preparingDownload": "正在準備下載...", "reconnect": "重新關聯", "reconnectTooltip": "與本地 LoRA 重新關聯", + "reconnectInstructions": "輸入 LoRA 語法或名稱以重新關聯:", + "reconnectExample": "範例: 或只填名稱", + "reconnectPlaceholder": "輸入 LoRA 名稱或語法", "viewOnCivitai": "在 CivitAI 上檢視", "openLoraDetails": "在 LoRA 庫中檢視 {name}", "openCheckpointDetails": "在模型庫中檢視 {name}" diff --git a/static/css/components/recipe-modal.css b/static/css/components/recipe-modal.css index 2212d045..691b0902 100644 --- a/static/css/components/recipe-modal.css +++ b/static/css/components/recipe-modal.css @@ -966,14 +966,14 @@ /* Deleted badge is a pure status indicator; the reconnect action lives on an explicit ghost button in the item's action row. */ -/* LoRA reconnect container */ +/* LoRA reconnect container: an inline extension of the item, not a nested + card — a dashed separator reads lighter than another bordered box inside + an already bordered item. */ .lora-reconnect-container { display: none; flex-direction: column; - background: var(--lora-surface); - border: 1px solid var(--border-color); - border-radius: var(--border-radius-xs); - padding: 12px; + border-top: 1px dashed var(--border-color); + padding-top: 10px; margin-top: 10px; gap: 10px; } @@ -1001,18 +1001,6 @@ font-size: 0.85em; } -.reconnect-instructions code { - background: rgba(0, 0, 0, 0.1); - padding: 2px 4px; - border-radius: 3px; - font-family: var(--font-mono); - font-size: 0.9em; -} - -[data-theme="dark"] .reconnect-instructions code { - background: rgba(255, 255, 255, 0.1); -} - .reconnect-form { display: flex; flex-direction: column; @@ -1020,7 +1008,8 @@ } .reconnect-input { - width: calc(100% - 20px); + box-sizing: border-box; + width: 100%; padding: 8px 10px; border: 1px solid var(--border-color); border-radius: var(--border-radius-xs); @@ -1029,6 +1018,17 @@ font-size: 0.9em; } +.reconnect-error { + display: none; + margin: 0; + color: var(--lora-error); + font-size: 0.85em; +} + +.reconnect-error.active { + display: block; +} + .reconnect-actions { display: flex; justify-content: flex-end; diff --git a/static/js/components/RecipeModal.js b/static/js/components/RecipeModal.js index 352e116a..ff7a9bb7 100644 --- a/static/js/components/RecipeModal.js +++ b/static/js/components/RecipeModal.js @@ -963,16 +963,17 @@ class RecipeModal { ${isDeleted || lora.hashInvalid ? `
-

Enter LoRA Syntax or Name to Reconnect:

- Example: <lora:Boris_Vallejo_BV_flux_D:1> or just Boris_Vallejo_BV_flux_D +

${escapeHtml(translate('recipes.resources.reconnectInstructions', {}, 'Enter LoRA syntax or name to reconnect:'))}

+ ${escapeHtml(translate('recipes.resources.reconnectExample', {}, 'Example: or just the name'))}
- +
- - + +
+
` : ''} @@ -1660,6 +1661,9 @@ class RecipeModal { // Add keydown handlers to reconnect inputs const reconnectInputs = document.querySelectorAll('.reconnect-input'); reconnectInputs.forEach(input => { + input.addEventListener('input', () => { + this.clearReconnectError(input.closest('.lora-reconnect-container')); + }); input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { const container = input.closest('.lora-reconnect-container'); @@ -1683,6 +1687,7 @@ class RecipeModal { const container = document.querySelector(`.lora-reconnect-container[data-lora-index="${loraIndex}"]`); if (container) { container.classList.add('active'); + this.clearReconnectError(container); const input = container.querySelector('.reconnect-input'); input.focus(); } @@ -1691,14 +1696,33 @@ class RecipeModal { hideReconnectInput(container) { if (container && container.classList.contains('active')) { container.classList.remove('active'); + this.clearReconnectError(container); const input = container.querySelector('.reconnect-input'); if (input) input.value = ''; } } + showReconnectError(container, message) { + const error = container && container.querySelector('.reconnect-error'); + if (error) { + error.textContent = message; + error.classList.add('active'); + } + } + + clearReconnectError(container) { + const error = container && container.querySelector('.reconnect-error'); + if (error) { + error.textContent = ''; + error.classList.remove('active'); + } + } + async reconnectLora(loraIndex, inputValue) { + const container = document.querySelector(`.lora-reconnect-container[data-lora-index="${loraIndex}"]`); + if (!inputValue || !inputValue.trim()) { - showToast('toast.recipes.enterLoraName', {}, 'error'); + this.showReconnectError(container, translate('toast.recipes.enterLoraName', {}, 'Please enter a LoRA name or syntax')); return; } @@ -1729,7 +1753,6 @@ class RecipeModal { if (result.success) { // Hide the reconnect input - const container = document.querySelector(`.lora-reconnect-container[data-lora-index="${loraIndex}"]`); this.hideReconnectInput(container); // Update the current recipe with the updated lora data @@ -1747,11 +1770,11 @@ class RecipeModal { loras: this.currentRecipe.loras }); } else { - showToast('toast.recipes.reconnectFailed', { message: result.error }, 'error'); + this.showReconnectError(container, translate('toast.recipes.reconnectFailed', { message: result.error }, `Error reconnecting LoRA: ${result.error}`)); } } catch (error) { console.error('Error reconnecting LoRA:', error); - showToast('toast.recipes.reconnectFailed', { message: error.message }, 'error'); + this.showReconnectError(container, translate('toast.recipes.reconnectFailed', { message: error.message }, `Error reconnecting LoRA: ${error.message}`)); } finally { state.loadingManager.hide(); } diff --git a/tests/frontend/components/recipeModal.resourceItems.test.js b/tests/frontend/components/recipeModal.resourceItems.test.js index 069ec8a9..d0db3ab7 100644 --- a/tests/frontend/components/recipeModal.resourceItems.test.js +++ b/tests/frontend/components/recipeModal.resourceItems.test.js @@ -330,6 +330,42 @@ describe('RecipeModal resource item interactions', () => { expect(container.classList.contains('active')).toBe(true); }); + it('shows reconnect failures inline in the panel instead of a toast', async () => { + const recipeModal = await createRecipeModal(); + global.fetch = vi.fn(async (url) => { + if (String(url).includes('/recipe/lora/reconnect')) { + return { ok: true, json: async () => ({ success: false, error: 'LoRA not found locally' }) }; + } + return { ok: true, json: async () => ({}) }; + }); + recipeModal.showRecipeDetails(recipeWithResources); + await flushWiring(); + + const deletedItem = document.querySelector('.recipe-lora-item.is-deleted'); + deletedItem.querySelector('.lora-reconnect').click(); + const container = deletedItem.querySelector('.lora-reconnect-container'); + const input = container.querySelector('.reconnect-input'); + const error = container.querySelector('.reconnect-error'); + expect(error).not.toBeNull(); + + input.value = 'nonexistent-lora'; + container.querySelector('.reconnect-confirm-btn').click(); + + await vi.waitFor(() => { + expect(error.classList.contains('active')).toBe(true); + }); + expect(error.textContent).toContain('LoRA not found locally'); + expect(showToastMock).not.toHaveBeenCalledWith( + 'toast.recipes.reconnectFailed', + expect.anything(), + 'error' + ); + + // Typing again clears the inline error + input.dispatchEvent(new Event('input', { bubbles: true })); + expect(error.classList.contains('active')).toBe(false); + expect(error.textContent).toBe(''); + }); it('renders hash-invalid LoRAs with a dedicated badge and reconnect instead of download', async () => { const recipeModal = await createRecipeModal(); recipeModal.showRecipeDetails(recipeWithResources);