fix(recipe): show checkpoint-linked recipes in model modal (#851)

This commit is contained in:
Will Miao
2026-03-31 16:45:01 +08:00
parent 316f17dd46
commit 8dc2a2f76b
12 changed files with 393 additions and 51 deletions

View File

@@ -19,7 +19,7 @@ import { renderCompactTags, setupTagTooltip, formatFileSize, escapeAttribute, es
import { renderTriggerWords, setupTriggerWordsEditMode } from './TriggerWords.js';
import { parsePresets, renderPresetTags } from './PresetTags.js';
import { initVersionsTab } from './ModelVersionsTab.js';
import { loadRecipesForLora } from './RecipeTab.js';
import { loadRecipesForModel } from './RecipeTab.js';
import { translate } from '../../utils/i18nHelpers.js';
import { state } from '../../state/index.js';
@@ -355,7 +355,9 @@ export async function showModelModal(model, modelType) {
${versionsTabBadge}
</button>`.trim();
const tabsContent = modelType === 'loras' ?
const supportsRecipesTab = modelType === 'loras' || modelType === 'checkpoints';
const tabsContent = supportsRecipesTab ?
`<button class="tab-btn active" data-tab="showcase">${examplesText}</button>
<button class="tab-btn" data-tab="description">${descriptionText}</button>
${versionsTabButton}
@@ -385,7 +387,7 @@ export async function showModelModal(model, modelType) {
</button>
</div>`.trim();
const tabPanesContent = modelType === 'loras' ?
const tabPanesContent = supportsRecipesTab ?
`<div id="showcase-tab" class="tab-pane active">
<div class="example-images-loading">
<i class="fas fa-spinner fa-spin"></i> ${loadingExampleImagesText}
@@ -664,14 +666,23 @@ export async function showModelModal(model, modelType) {
setupNavigationShortcuts(modelType);
updateNavigationControls();
// LoRA specific setup
// Model-specific setup
if (modelType === 'loras' || modelType === 'embeddings') {
setupTriggerWordsEditMode();
}
if (modelType == 'loras') {
// Load recipes for this LoRA
loadRecipesForLora(modelWithFullData.model_name, modelWithFullData.sha256);
}
if (modelType === 'loras') {
loadRecipesForModel({
modelKind: 'lora',
displayName: modelWithFullData.model_name,
sha256: modelWithFullData.sha256,
});
} else if (modelType === 'checkpoints') {
loadRecipesForModel({
modelKind: 'checkpoint',
displayName: modelWithFullData.model_name,
sha256: modelWithFullData.sha256,
});
}
// Load example images asynchronously - merge regular and custom images

View File

@@ -1,38 +1,47 @@
/**
* RecipeTab - Handles the recipes tab in model modals (LoRA specific functionality)
* Moved to shared directory for consistency
* RecipeTab - Handles the recipes tab in model modals.
*/
import { showToast, copyToClipboard } from '../../utils/uiHelpers.js';
import { setSessionItem, removeSessionItem } from '../../utils/storageHelpers.js';
/**
* Loads recipes that use the specified Lora and renders them in the tab
* @param {string} loraName - The display name of the Lora
* @param {string} sha256 - The SHA256 hash of the Lora
* Loads recipes that use the specified model and renders them in the tab.
* @param {Object} options
* @param {'lora'|'checkpoint'} options.modelKind - Model kind for copy and endpoint selection
* @param {string} options.displayName - The display name of the model
* @param {string} options.sha256 - The SHA256 hash of the model
*/
export function loadRecipesForLora(loraName, sha256) {
export function loadRecipesForModel({ modelKind, displayName, sha256 }) {
const recipeTab = document.getElementById('recipes-tab');
if (!recipeTab) return;
const normalizedHash = sha256?.toLowerCase?.() || '';
const modelLabel = getModelLabel(modelKind);
// Show loading state
recipeTab.innerHTML = `
<div class="recipes-loading">
<i class="fas fa-spinner fa-spin"></i> Loading recipes...
</div>
`;
// Fetch recipes that use this Lora by hash
fetch(`/api/lm/recipes/for-lora?hash=${encodeURIComponent(sha256.toLowerCase())}`)
// Fetch recipes that use this model by hash
fetch(`${getRecipesEndpoint(modelKind)}?hash=${encodeURIComponent(normalizedHash)}`)
.then(response => response.json())
.then(data => {
if (!data.success) {
throw new Error(data.error || 'Failed to load recipes');
}
renderRecipes(recipeTab, data.recipes, loraName, sha256);
renderRecipes(recipeTab, data.recipes, {
modelKind,
displayName,
modelHash: normalizedHash,
modelLabel,
});
})
.catch(error => {
console.error('Error loading recipes for Lora:', error);
console.error(`Error loading recipes for ${modelLabel}:`, error);
recipeTab.innerHTML = `
<div class="recipes-error">
<i class="fas fa-exclamation-circle"></i>
@@ -46,18 +55,24 @@ export function loadRecipesForLora(loraName, sha256) {
* Renders the recipe cards in the tab
* @param {HTMLElement} tabElement - The tab element to render into
* @param {Array} recipes - Array of recipe objects
* @param {string} loraName - The display name of the Lora
* @param {string} loraHash - The hash of the Lora
* @param {Object} options - Render options
*/
function renderRecipes(tabElement, recipes, loraName, loraHash) {
function renderRecipes(tabElement, recipes, options) {
const {
modelKind,
displayName,
modelHash,
modelLabel,
} = options;
if (!recipes || recipes.length === 0) {
tabElement.innerHTML = `
<div class="recipes-empty">
<i class="fas fa-book-open"></i>
<p>No recipes found that use this Lora.</p>
<p>No recipes found that use this ${modelLabel}.</p>
</div>
`;
return;
}
@@ -73,13 +88,13 @@ function renderRecipes(tabElement, recipes, loraName, loraHash) {
headerText.appendChild(eyebrow);
const title = document.createElement('h3');
title.textContent = `${recipes.length} recipe${recipes.length > 1 ? 's' : ''} using this Lora`;
title.textContent = `${recipes.length} recipe${recipes.length > 1 ? 's' : ''} using this ${modelLabel}`;
headerText.appendChild(title);
const description = document.createElement('p');
description.className = 'recipes-header__description';
description.textContent = loraName ?
`Discover workflows crafted for ${loraName}.` :
description.textContent = displayName ?
`Discover workflows crafted for ${displayName}.` :
'Discover workflows crafted for this model.';
headerText.appendChild(description);
@@ -101,7 +116,11 @@ function renderRecipes(tabElement, recipes, loraName, loraHash) {
headerElement.appendChild(viewAllButton);
viewAllButton.addEventListener('click', () => {
navigateToRecipesPage(loraName, loraHash);
navigateToRecipesPage({
modelKind,
displayName,
modelHash,
});
});
const cardGrid = document.createElement('div');
@@ -280,26 +299,32 @@ function copyRecipeSyntax(recipeId) {
}
/**
* Navigates to the recipes page with filter for the current Lora
* @param {string} loraName - The Lora display name to filter by
* @param {string} loraHash - The hash of the Lora to filter by
* @param {boolean} createNew - Whether to open the create recipe dialog
* Navigates to the recipes page with filter for the current model
* @param {Object} options - Navigation options
*/
function navigateToRecipesPage(loraName, loraHash) {
function navigateToRecipesPage({ modelKind, displayName, modelHash }) {
// Close the current modal
if (window.modalManager) {
modalManager.closeModal('modelModal');
}
// Clear any previous filters first
removeSessionItem('lora_to_recipe_filterLoraName');
removeSessionItem('lora_to_recipe_filterLoraHash');
removeSessionItem('checkpoint_to_recipe_filterCheckpointName');
removeSessionItem('checkpoint_to_recipe_filterCheckpointHash');
removeSessionItem('viewRecipeId');
// Store the LoRA name and hash filter in sessionStorage
setSessionItem('lora_to_recipe_filterLoraName', loraName);
setSessionItem('lora_to_recipe_filterLoraHash', loraHash);
if (modelKind === 'checkpoint') {
// Store the checkpoint name and hash filter in sessionStorage
setSessionItem('checkpoint_to_recipe_filterCheckpointName', displayName);
setSessionItem('checkpoint_to_recipe_filterCheckpointHash', modelHash);
} else {
// Store the LoRA name and hash filter in sessionStorage
setSessionItem('lora_to_recipe_filterLoraName', displayName);
setSessionItem('lora_to_recipe_filterLoraHash', modelHash);
}
// Directly navigate to recipes page
window.location.href = '/loras/recipes';
}
@@ -321,7 +346,18 @@ function navigateToRecipeDetails(recipeId) {
// Store the recipe ID in sessionStorage to load on recipes page
setSessionItem('viewRecipeId', recipeId);
// Directly navigate to recipes page
window.location.href = '/loras/recipes';
}
function getRecipesEndpoint(modelKind) {
if (modelKind === 'checkpoint') {
return '/api/lm/recipes/for-checkpoint';
}
return '/api/lm/recipes/for-lora';
}
function getModelLabel(modelKind) {
return modelKind === 'checkpoint' ? 'checkpoint' : 'LoRA';
}