diff --git a/locales/de.json b/locales/de.json index 3ff8cdd4..c6c1bdc8 100644 --- a/locales/de.json +++ b/locales/de.json @@ -528,6 +528,9 @@ }, "recipes": { "title": "LoRA-Rezepte", + "actions": { + "sendCheckpoint": "Send to ComfyUI" + }, "controls": { "import": { "action": "Importieren", @@ -1254,6 +1257,9 @@ "cannotSend": "Kann Rezept nicht senden: Fehlende Rezept-ID", "sendFailed": "Fehler beim Senden des Rezepts an Workflow", "sendError": "Fehler beim Senden des Rezepts an Workflow", + "missingCheckpointPath": "Checkpoint-Pfad nicht verfügbar", + "missingCheckpointInfo": "Checkpoint-Informationen fehlen", + "downloadCheckpointFailed": "Checkpoint-Download fehlgeschlagen: {message}", "cannotDelete": "Kann Rezept nicht löschen: Fehlende Rezept-ID", "deleteConfirmationError": "Fehler beim Anzeigen der Löschbestätigung", "deletedSuccessfully": "Rezept erfolgreich gelöscht", diff --git a/locales/en.json b/locales/en.json index 4596e45f..dde88b82 100644 --- a/locales/en.json +++ b/locales/en.json @@ -528,6 +528,9 @@ }, "recipes": { "title": "LoRA Recipes", + "actions": { + "sendCheckpoint": "Send to ComfyUI" + }, "controls": { "import": { "action": "Import", @@ -1254,6 +1257,9 @@ "cannotSend": "Cannot send recipe: Missing recipe ID", "sendFailed": "Failed to send recipe to workflow", "sendError": "Error sending recipe to workflow", + "missingCheckpointPath": "Checkpoint path not available", + "missingCheckpointInfo": "Missing checkpoint information", + "downloadCheckpointFailed": "Failed to download checkpoint: {message}", "cannotDelete": "Cannot delete recipe: Missing recipe ID", "deleteConfirmationError": "Error showing delete confirmation", "deletedSuccessfully": "Recipe deleted successfully", diff --git a/locales/es.json b/locales/es.json index dcdfb26b..08596127 100644 --- a/locales/es.json +++ b/locales/es.json @@ -528,6 +528,9 @@ }, "recipes": { "title": "Recetas de LoRA", + "actions": { + "sendCheckpoint": "Enviar a ComfyUI" + }, "controls": { "import": { "action": "Importar", @@ -1254,6 +1257,9 @@ "cannotSend": "No se puede enviar receta: Falta ID de receta", "sendFailed": "Error al enviar receta al flujo de trabajo", "sendError": "Error enviando receta al flujo de trabajo", + "missingCheckpointPath": "Ruta del checkpoint no disponible", + "missingCheckpointInfo": "Falta información del checkpoint", + "downloadCheckpointFailed": "Error al descargar el checkpoint: {message}", "cannotDelete": "No se puede eliminar receta: Falta ID de receta", "deleteConfirmationError": "Error mostrando confirmación de eliminación", "deletedSuccessfully": "Receta eliminada exitosamente", diff --git a/locales/fr.json b/locales/fr.json index 90452dc9..275d0d5c 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -528,6 +528,9 @@ }, "recipes": { "title": "LoRA Recipes", + "actions": { + "sendCheckpoint": "Envoyer vers ComfyUI" + }, "controls": { "import": { "action": "Importer", @@ -1254,6 +1257,9 @@ "cannotSend": "Impossible d'envoyer la recipe : ID de recipe manquant", "sendFailed": "Échec de l'envoi de la recipe vers le workflow", "sendError": "Erreur lors de l'envoi de la recipe vers le workflow", + "missingCheckpointPath": "Chemin du checkpoint indisponible", + "missingCheckpointInfo": "Informations sur le checkpoint manquantes", + "downloadCheckpointFailed": "Échec du téléchargement du checkpoint : {message}", "cannotDelete": "Impossible de supprimer la recipe : ID de recipe manquant", "deleteConfirmationError": "Erreur lors de l'affichage de la confirmation de suppression", "deletedSuccessfully": "Recipe supprimée avec succès", diff --git a/locales/he.json b/locales/he.json index 33b5bb9f..f672b1b4 100644 --- a/locales/he.json +++ b/locales/he.json @@ -528,6 +528,9 @@ }, "recipes": { "title": "מתכוני LoRA", + "actions": { + "sendCheckpoint": "שלח ל-ComfyUI" + }, "controls": { "import": { "action": "ייבא", @@ -1254,6 +1257,9 @@ "cannotSend": "לא ניתן לשלוח מתכון: חסר מזהה מתכון", "sendFailed": "שליחת המתכון ל-workflow נכשלה", "sendError": "שגיאה בשליחת המתכון ל-workflow", + "missingCheckpointPath": "נתיב ה-checkpoint אינו זמין", + "missingCheckpointInfo": "חסרים פרטי checkpoint", + "downloadCheckpointFailed": "הורדת checkpoint נכשלה: {message}", "cannotDelete": "לא ניתן למחוק מתכון: חסר מזהה מתכון", "deleteConfirmationError": "שגיאה בהצגת אישור המחיקה", "deletedSuccessfully": "המתכון נמחק בהצלחה", diff --git a/locales/ja.json b/locales/ja.json index 178ca774..c592fc9c 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -528,6 +528,9 @@ }, "recipes": { "title": "LoRAレシピ", + "actions": { + "sendCheckpoint": "ComfyUIへ送信" + }, "controls": { "import": { "action": "インポート", @@ -1254,6 +1257,9 @@ "cannotSend": "レシピを送信できません:レシピIDがありません", "sendFailed": "レシピのワークフローへの送信に失敗しました", "sendError": "レシピのワークフロー送信エラー", + "missingCheckpointPath": "チェックポイントのパスがありません", + "missingCheckpointInfo": "チェックポイント情報が不足しています", + "downloadCheckpointFailed": "チェックポイントのダウンロードに失敗しました: {message}", "cannotDelete": "レシピを削除できません:レシピIDがありません", "deleteConfirmationError": "削除確認の表示中にエラーが発生しました", "deletedSuccessfully": "レシピが正常に削除されました", diff --git a/locales/ko.json b/locales/ko.json index 394c2bed..1baee1b2 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -528,6 +528,9 @@ }, "recipes": { "title": "LoRA 레시피", + "actions": { + "sendCheckpoint": "ComfyUI로 보내기" + }, "controls": { "import": { "action": "가져오기", @@ -1254,6 +1257,9 @@ "cannotSend": "레시피를 전송할 수 없습니다: 레시피 ID 누락", "sendFailed": "레시피를 워크플로로 전송하는데 실패했습니다", "sendError": "레시피를 워크플로로 전송하는 중 오류", + "missingCheckpointPath": "체크포인트 경로를 사용할 수 없습니다", + "missingCheckpointInfo": "체크포인트 정보가 부족합니다", + "downloadCheckpointFailed": "체크포인트 다운로드 실패: {message}", "cannotDelete": "레시피를 삭제할 수 없습니다: 레시피 ID 누락", "deleteConfirmationError": "삭제 확인 표시 오류", "deletedSuccessfully": "레시피가 성공적으로 삭제되었습니다", diff --git a/locales/ru.json b/locales/ru.json index 6c3e1587..b21e69dd 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -528,6 +528,9 @@ }, "recipes": { "title": "Рецепты LoRA", + "actions": { + "sendCheckpoint": "Отправить в ComfyUI" + }, "controls": { "import": { "action": "Импортировать", @@ -1254,6 +1257,9 @@ "cannotSend": "Невозможно отправить рецепт: отсутствует ID рецепта", "sendFailed": "Не удалось отправить рецепт в workflow", "sendError": "Ошибка отправки рецепта в workflow", + "missingCheckpointPath": "Путь к чекпойнту недоступен", + "missingCheckpointInfo": "Отсутствуют данные о чекпойнте", + "downloadCheckpointFailed": "Не удалось скачать чекпойнт: {message}", "cannotDelete": "Невозможно удалить рецепт: отсутствует ID рецепта", "deleteConfirmationError": "Ошибка отображения подтверждения удаления", "deletedSuccessfully": "Рецепт успешно удален", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 1bc61118..ddbf523d 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -528,6 +528,9 @@ }, "recipes": { "title": "LoRA 配方", + "actions": { + "sendCheckpoint": "发送到 ComfyUI" + }, "controls": { "import": { "action": "导入", @@ -1254,6 +1257,9 @@ "cannotSend": "无法发送配方:缺少配方 ID", "sendFailed": "发送配方到工作流失败", "sendError": "发送配方到工作流出错", + "missingCheckpointPath": "缺少检查点路径", + "missingCheckpointInfo": "缺少检查点信息", + "downloadCheckpointFailed": "下载检查点失败:{message}", "cannotDelete": "无法删除配方:缺少配方 ID", "deleteConfirmationError": "显示删除确认出错", "deletedSuccessfully": "配方删除成功", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index a524c3e2..dcb4a3f4 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -528,6 +528,9 @@ }, "recipes": { "title": "LoRA 配方", + "actions": { + "sendCheckpoint": "傳送到 ComfyUI" + }, "controls": { "import": { "action": "匯入", @@ -1254,6 +1257,9 @@ "cannotSend": "無法傳送配方:缺少配方 ID", "sendFailed": "傳送配方到工作流失敗", "sendError": "傳送配方到工作流錯誤", + "missingCheckpointPath": "缺少檢查點路徑", + "missingCheckpointInfo": "缺少檢查點資訊", + "downloadCheckpointFailed": "下載檢查點失敗:{message}", "cannotDelete": "無法刪除配方:缺少配方 ID", "deleteConfirmationError": "顯示刪除確認時發生錯誤", "deletedSuccessfully": "配方已成功刪除", diff --git a/py/routes/checkpoint_routes.py b/py/routes/checkpoint_routes.py index 16ebd338..e22ab2a3 100644 --- a/py/routes/checkpoint_routes.py +++ b/py/routes/checkpoint_routes.py @@ -1,4 +1,5 @@ import logging +from typing import Dict from aiohttp import web from .base_model_routes import BaseModelRoutes @@ -51,6 +52,19 @@ class CheckpointRoutes(BaseModelRoutes): def _get_expected_model_types(self) -> str: """Get expected model types string for error messages""" return "Checkpoint" + + def _parse_specific_params(self, request: web.Request) -> Dict: + """Parse Checkpoint-specific parameters""" + params: Dict = {} + + if 'checkpoint_hash' in request.query: + params['hash_filters'] = {'single_hash': request.query['checkpoint_hash'].lower()} + elif 'checkpoint_hashes' in request.query: + params['hash_filters'] = { + 'multiple_hashes': [h.lower() for h in request.query['checkpoint_hashes'].split(',')] + } + + return params async def get_checkpoint_info(self, request: web.Request) -> web.Response: """Get detailed information for a specific checkpoint by name""" diff --git a/static/css/components/recipe-modal.css b/static/css/components/recipe-modal.css index edcec091..f561abe8 100644 --- a/static/css/components/recipe-modal.css +++ b/static/css/components/recipe-modal.css @@ -635,7 +635,7 @@ } .recipe-lora-item.checkpoint-item { - cursor: default; + cursor: pointer; padding-top: 8px; padding-bottom: 8px; align-items: center; diff --git a/static/js/api/baseModelApi.js b/static/js/api/baseModelApi.js index ec0b6289..e651b4dc 100644 --- a/static/js/api/baseModelApi.js +++ b/static/js/api/baseModelApi.js @@ -877,6 +877,21 @@ export class BaseModelApiClient { console.error('Error parsing lora hashes from session storage:', error); } } + } else if (this.modelType === 'checkpoints') { + const filterCheckpointHash = getSessionItem('recipe_to_checkpoint_filterHash'); + const filterCheckpointHashes = getSessionItem('recipe_to_checkpoint_filterHashes'); + + if (filterCheckpointHash) { + params.append('checkpoint_hash', filterCheckpointHash); + } else if (filterCheckpointHashes) { + try { + if (Array.isArray(filterCheckpointHashes) && filterCheckpointHashes.length > 0) { + params.append('checkpoint_hashes', filterCheckpointHashes.join(',')); + } + } catch (error) { + console.error('Error parsing checkpoint hashes from session storage:', error); + } + } } } diff --git a/static/js/components/RecipeModal.js b/static/js/components/RecipeModal.js index 299a2a11..7e909883 100644 --- a/static/js/components/RecipeModal.js +++ b/static/js/components/RecipeModal.js @@ -351,6 +351,7 @@ class RecipeModal { if (recipe.checkpoint && typeof recipe.checkpoint === 'object') { checkpointContainer.innerHTML = this.renderCheckpoint(recipe.checkpoint); this.setupCheckpointActions(checkpointContainer, recipe.checkpoint); + this.setupCheckpointNavigation(checkpointContainer, recipe.checkpoint); } } @@ -1150,6 +1151,15 @@ class RecipeModal { } } + setupCheckpointNavigation(container, checkpoint) { + const checkpointItem = container.querySelector('.checkpoint-item'); + if (!checkpointItem) return; + + checkpointItem.addEventListener('click', () => { + this.navigateToCheckpointPage(checkpoint); + }); + } + canDownloadCheckpoint(checkpoint) { if (!checkpoint) return false; const modelId = checkpoint.modelId || checkpoint.modelID || checkpoint.model_id; @@ -1238,8 +1248,42 @@ class RecipeModal { } } + navigateToCheckpointPage(checkpoint) { + const checkpointHash = this._getCheckpointHash(checkpoint); + + if (!checkpointHash) { + showToast('toast.recipes.missingCheckpointInfo', {}, 'error'); + return; + } + + modalManager.closeModal('recipeModal'); + + removeSessionItem('recipe_to_checkpoint_filterHash'); + removeSessionItem('recipe_to_checkpoint_filterHashes'); + removeSessionItem('filterCheckpointRecipeName'); + + setSessionItem('recipe_to_checkpoint_filterHash', checkpointHash.toLowerCase()); + if (this.currentRecipe?.title) { + setSessionItem('filterCheckpointRecipeName', this.currentRecipe.title); + } + + window.location.href = '/checkpoints'; + } + + _getCheckpointHash(checkpoint) { + if (!checkpoint) return ''; + const hash = + checkpoint.hash || + checkpoint.sha256 || + checkpoint.sha256_hash || + checkpoint.sha256Hash || + checkpoint.SHA256; + return hash ? hash.toString() : ''; + } + // New method to navigate to the LoRAs page navigateToLorasPage(specificLoraIndex = null) { + debugger; // Close the current modal modalManager.closeModal('recipeModal'); @@ -1278,7 +1322,7 @@ class RecipeModal { // New method to make LoRA items clickable setupLoraItemsClickable() { - const loraItems = document.querySelectorAll('.recipe-lora-item'); + const loraItems = document.querySelectorAll('.recipe-lora-item:not(.checkpoint-item)'); loraItems.forEach(item => { // Get the lora index from the data attribute const loraIndex = parseInt(item.dataset.loraIndex); diff --git a/static/js/components/controls/CheckpointsControls.js b/static/js/components/controls/CheckpointsControls.js index fa1e48e2..079037ff 100644 --- a/static/js/components/controls/CheckpointsControls.js +++ b/static/js/components/controls/CheckpointsControls.js @@ -1,7 +1,7 @@ // CheckpointsControls.js - Specific implementation for the Checkpoints page import { PageControls } from './PageControls.js'; import { getModelApiClient, resetAndReload } from '../../api/modelApiFactory.js'; -import { showToast } from '../../utils/uiHelpers.js'; +import { getSessionItem, removeSessionItem } from '../../utils/storageHelpers.js'; import { downloadManager } from '../../managers/DownloadManager.js'; /** @@ -14,6 +14,9 @@ export class CheckpointsControls extends PageControls { // Register API methods specific to the Checkpoints page this.registerCheckpointsAPI(); + + // Check for custom filters (e.g., from recipe navigation) + this.checkCustomFilters(); } /** @@ -52,14 +55,65 @@ export class CheckpointsControls extends PageControls { } }, - // No clearCustomFilter implementation is needed for checkpoints - // as custom filters are currently only used for LoRAs clearCustomFilter: async () => { - showToast('toast.filters.noCustomFilterToClear', {}, 'info'); + await this.clearCustomFilter(); } }; // Register the API this.registerAPI(checkpointsAPI); } -} \ No newline at end of file + + /** + * Check for custom filters sent from other pages (e.g., recipe modal) + */ + checkCustomFilters() { + const filterCheckpointHash = getSessionItem('recipe_to_checkpoint_filterHash'); + const filterRecipeName = getSessionItem('filterCheckpointRecipeName'); + + if (filterCheckpointHash && filterRecipeName) { + const indicator = document.getElementById('customFilterIndicator'); + const filterText = indicator?.querySelector('.customFilterText'); + + if (indicator && filterText) { + indicator.classList.remove('hidden'); + + const displayText = `Viewing checkpoint from: ${filterRecipeName}`; + filterText.textContent = this._truncateText(displayText, 30); + filterText.setAttribute('title', displayText); + + const filterElement = indicator.querySelector('.filter-active'); + if (filterElement) { + filterElement.classList.add('animate'); + setTimeout(() => filterElement.classList.remove('animate'), 600); + } + } + } + } + + /** + * Clear checkpoint custom filter and reload + */ + async clearCustomFilter() { + removeSessionItem('recipe_to_checkpoint_filterHash'); + removeSessionItem('recipe_to_checkpoint_filterHashes'); + removeSessionItem('filterCheckpointRecipeName'); + + const indicator = document.getElementById('customFilterIndicator'); + if (indicator) { + indicator.classList.add('hidden'); + } + + await resetAndReload(); + } + + /** + * Helper to truncate text with ellipsis + * @param {string} text + * @param {number} maxLength + * @returns {string} + */ + _truncateText(text, maxLength) { + return text.length > maxLength ? `${text.substring(0, maxLength - 3)}...` : text; + } +} diff --git a/tests/frontend/components/pageControls.filtering.test.js b/tests/frontend/components/pageControls.filtering.test.js index 3954a394..678cea51 100644 --- a/tests/frontend/components/pageControls.filtering.test.js +++ b/tests/frontend/components/pageControls.filtering.test.js @@ -62,6 +62,8 @@ vi.mock('../../../static/js/utils/updateCheckHelpers.js', () => ({ beforeEach(() => { vi.resetModules(); vi.clearAllMocks(); + localStorage.clear(); + sessionStorage.clear(); loadMoreWithVirtualScrollMock.mockResolvedValue(undefined); refreshModelsMock.mockResolvedValue(undefined); @@ -163,6 +165,8 @@ function renderControlsDom(pageKey) { @@ -518,6 +522,35 @@ describe('PageControls favorites, sorting, and duplicates scenarios', () => { expect(stateModule.getCurrentPageState().sortBy).toBe('date:desc'); }); + it('shows checkpoint custom filter indicator from recipes and clears it', async () => { + renderControlsDom('checkpoints'); + const stateModule = await import('../../../static/js/state/index.js'); + stateModule.initPageState('checkpoints'); + + sessionStorage.setItem('lora_manager_recipe_to_checkpoint_filterHash', 'abc123'); + sessionStorage.setItem('lora_manager_filterCheckpointRecipeName', 'Flux Recipe With Long Name'); + + const { CheckpointsControls } = await import('../../../static/js/components/controls/CheckpointsControls.js'); + + const controls = new CheckpointsControls(); + + const indicator = document.getElementById('customFilterIndicator'); + expect(indicator.classList.contains('hidden')).toBe(false); + + const filterText = indicator.querySelector('.customFilterText'); + expect(filterText.textContent.startsWith('Viewing checkpoint from:')).toBe(true); + expect(filterText.getAttribute('title')).toBe('Viewing checkpoint from: Flux Recipe With Long Name'); + + resetAndReloadMock.mockClear(); + + await controls.clearCustomFilter(); + + expect(sessionStorage.getItem('lora_manager_recipe_to_checkpoint_filterHash')).toBeNull(); + expect(sessionStorage.getItem('lora_manager_filterCheckpointRecipeName')).toBeNull(); + expect(indicator.classList.contains('hidden')).toBe(true); + expect(resetAndReloadMock).toHaveBeenCalled(); + }); + it('updates duplicate badge after refresh and toggles duplicate mode from controls', async () => { renderControlsDom('checkpoints'); const stateModule = await import('../../../static/js/state/index.js');