mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-08-29 17:01:26 -03:00
fix(recipes): distinguish unobtainable LoRAs in recipe status and skip them in syntax
The recipe card pill counted LoRAs deleted from the source (isDeleted) as available, showing a green 'ready 2/2' for recipes that cannot be fully reproduced. LoRAs with an unresolvable hash (hashInvalid) were counted as missing/downloadable even though downloads always fail, and recipe syntax generation emitted broken tokens for them. - Four-state status on RecipeCard pill and RecipeTab badge: ready (all in library), missing (downloadable, red, keeps the action cue), partial (unobtainable entries skipped when used, amber, fa-circle-minus), unavailable (nothing usable, gray, fa-ban) - Pill numerator is now the real in-library count; tooltips spell out missing vs unavailable (deleted from source or unresolvable hash) - get_recipe_syntax_tokens skips hashInvalid entries like deleted ones instead of emitting tokens pointing at nonexistent files - Bulk missing-download manager and recipe context menu exclude hashInvalid LoRAs, matching the modal's per-item download block - New locale keys loraStatus.missingAndUnavailable/partial/noneUsable, translated for all 9 non-en locales
This commit is contained in:
@@ -204,8 +204,9 @@ export class RecipeContextMenu extends BaseContextMenu {
|
||||
const response = await fetch(`/api/lm/recipe/${recipeId}`);
|
||||
const recipe = await response.json();
|
||||
|
||||
// Get missing LoRAs
|
||||
const missingLoras = recipe.loras.filter(lora => !lora.inLibrary && !lora.isDeleted);
|
||||
// Get missing LoRAs (still downloadable: not deleted from the
|
||||
// source and hash still resolvable)
|
||||
const missingLoras = recipe.loras.filter(lora => !lora.inLibrary && !lora.isDeleted && !lora.hashInvalid);
|
||||
|
||||
if (missingLoras.length === 0) {
|
||||
showToast('recipes.contextMenu.downloadMissing.noMissingLoras', {}, 'info');
|
||||
|
||||
@@ -41,15 +41,35 @@ class RecipeCard {
|
||||
const loras = this.recipe.loras || [];
|
||||
const lorasCount = loras.length;
|
||||
|
||||
// Check if all LoRAs are available in the library
|
||||
const missingLorasCount = loras.filter(lora => !lora.inLibrary && !lora.isDeleted).length;
|
||||
const allLorasAvailable = missingLorasCount === 0 && lorasCount > 0;
|
||||
// Count LoRAs by availability: in library, missing (still downloadable
|
||||
// from the source), or unobtainable (deleted from the source, or an
|
||||
// unresolvable hash) which is silently skipped when the recipe is used.
|
||||
const availableLorasCount = loras.filter(lora => lora.inLibrary).length;
|
||||
const missingLorasCount = loras.filter(lora => !lora.inLibrary && !lora.isDeleted && !lora.hashInvalid).length;
|
||||
const unavailableLorasCount = lorasCount - availableLorasCount - missingLorasCount;
|
||||
|
||||
// Compact status pill: state icon + available/total fraction.
|
||||
// Icon switches by state so status never relies on color alone.
|
||||
const availableLorasCount = lorasCount - missingLorasCount;
|
||||
const loraCountStateClass = missingLorasCount > 0 ? 'missing' : (allLorasAvailable ? 'ready' : '');
|
||||
const loraCountIcon = missingLorasCount > 0 ? 'fa-exclamation-triangle' : (allLorasAvailable ? 'fa-check' : 'fa-layer-group');
|
||||
// - missing (red): something can still be downloaded, most actionable
|
||||
// - partial (amber): usable but degraded, unobtainable LoRAs are skipped
|
||||
// - unavailable (gray, ban): no usable LoRA at all
|
||||
let loraCountStateClass = '';
|
||||
let loraCountIcon = 'fa-layer-group';
|
||||
if (lorasCount > 0) {
|
||||
if (availableLorasCount === lorasCount) {
|
||||
loraCountStateClass = 'ready';
|
||||
loraCountIcon = 'fa-check';
|
||||
} else if (missingLorasCount > 0) {
|
||||
loraCountStateClass = 'missing';
|
||||
loraCountIcon = 'fa-exclamation-triangle';
|
||||
} else if (availableLorasCount > 0) {
|
||||
loraCountStateClass = 'partial';
|
||||
loraCountIcon = 'fa-circle-minus';
|
||||
} else {
|
||||
loraCountStateClass = 'unavailable';
|
||||
loraCountIcon = 'fa-ban';
|
||||
}
|
||||
}
|
||||
const loraCountLabel = lorasCount > 0 ? `${availableLorasCount}/${lorasCount}` : `${lorasCount}`;
|
||||
|
||||
// Ensure file_url exists, fallback to API URL if needed
|
||||
@@ -135,7 +155,7 @@ class RecipeCard {
|
||||
<span class="model-name">${this.recipe.title}</span>
|
||||
</div>
|
||||
${!isDuplicatesMode ? `
|
||||
<div class="lora-count ${loraCountStateClass}" title="${this.getLoraStatusTitle(lorasCount, missingLorasCount)}">
|
||||
<div class="lora-count ${loraCountStateClass}" title="${this.getLoraStatusTitle(lorasCount, availableLorasCount, missingLorasCount, unavailableLorasCount)}">
|
||||
<i class="fas ${loraCountIcon}" aria-hidden="true"></i> ${loraCountLabel}
|
||||
</div>
|
||||
` : ''}
|
||||
@@ -154,17 +174,38 @@ class RecipeCard {
|
||||
return card;
|
||||
}
|
||||
|
||||
getLoraStatusTitle(totalCount, missingCount) {
|
||||
getLoraStatusTitle(totalCount, availableCount, missingCount, unavailableCount) {
|
||||
if (totalCount === 0) {
|
||||
return translate('recipes.loraStatus.none', {}, 'No LoRAs in this recipe');
|
||||
}
|
||||
if (missingCount === 0) {
|
||||
if (availableCount === totalCount) {
|
||||
return translate('recipes.loraStatus.allAvailable', {}, 'All LoRAs available - Ready to use');
|
||||
}
|
||||
if (missingCount > 0 && unavailableCount > 0) {
|
||||
return translate(
|
||||
'recipes.loraStatus.missingAndUnavailable',
|
||||
{ missing: missingCount, unavailable: unavailableCount, total: totalCount },
|
||||
`${missingCount} of ${totalCount} LoRAs missing, ${unavailableCount} unavailable (deleted from source or unresolvable hash)`
|
||||
);
|
||||
}
|
||||
if (missingCount > 0) {
|
||||
return translate(
|
||||
'recipes.loraStatus.missing',
|
||||
{ missing: missingCount, total: totalCount },
|
||||
`${missingCount} of ${totalCount} LoRAs missing`
|
||||
);
|
||||
}
|
||||
if (availableCount > 0) {
|
||||
return translate(
|
||||
'recipes.loraStatus.partial',
|
||||
{ unavailable: unavailableCount, total: totalCount },
|
||||
`${unavailableCount} of ${totalCount} LoRAs unavailable (deleted from source or unresolvable hash) - skipped when recipe is used`
|
||||
);
|
||||
}
|
||||
return translate(
|
||||
'recipes.loraStatus.missing',
|
||||
{ missing: missingCount, total: totalCount },
|
||||
`${missingCount} of ${totalCount} LoRAs missing`
|
||||
'recipes.loraStatus.noneUsable',
|
||||
{ unavailable: unavailableCount, total: totalCount },
|
||||
`No usable LoRAs - ${unavailableCount} of ${totalCount} deleted from source or unresolvable hash`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -130,17 +130,29 @@ function renderRecipes(tabElement, recipes, options) {
|
||||
const baseModel = recipe.base_model || '';
|
||||
const loras = recipe.loras || [];
|
||||
const lorasCount = loras.length;
|
||||
const missingLorasCount = loras.filter(lora => !lora.inLibrary && !lora.isDeleted).length;
|
||||
const allLorasAvailable = missingLorasCount === 0 && lorasCount > 0;
|
||||
const statusClass = lorasCount === 0 ? 'empty' : (allLorasAvailable ? 'ready' : 'missing');
|
||||
// Missing = still downloadable; unavailable = deleted from the source
|
||||
// or unresolvable hash, silently skipped when the recipe is used.
|
||||
const availableLorasCount = loras.filter(lora => lora.inLibrary).length;
|
||||
const missingLorasCount = loras.filter(lora => !lora.inLibrary && !lora.isDeleted && !lora.hashInvalid).length;
|
||||
const unavailableLorasCount = lorasCount - availableLorasCount - missingLorasCount;
|
||||
const statusClass = lorasCount === 0 ? 'empty'
|
||||
: (availableLorasCount === lorasCount ? 'ready'
|
||||
: (missingLorasCount > 0 ? 'missing'
|
||||
: (availableLorasCount > 0 ? 'partial' : 'unavailable')));
|
||||
let statusLabel;
|
||||
|
||||
if (lorasCount === 0) {
|
||||
statusLabel = 'No linked LoRAs';
|
||||
} else if (allLorasAvailable) {
|
||||
} else if (statusClass === 'ready') {
|
||||
statusLabel = `${lorasCount} LoRA${lorasCount > 1 ? 's' : ''} ready`;
|
||||
} else if (statusClass === 'missing') {
|
||||
statusLabel = unavailableLorasCount > 0
|
||||
? `Missing ${missingLorasCount}, ${unavailableLorasCount} of ${lorasCount} unavailable`
|
||||
: `Missing ${missingLorasCount} of ${lorasCount}`;
|
||||
} else if (statusClass === 'partial') {
|
||||
statusLabel = `${unavailableLorasCount} of ${lorasCount} unavailable - skipped when used`;
|
||||
} else {
|
||||
statusLabel = `Missing ${missingLorasCount} of ${lorasCount}`;
|
||||
statusLabel = 'No usable LoRAs';
|
||||
}
|
||||
|
||||
const imageUrl = recipe.file_url ||
|
||||
@@ -207,8 +219,16 @@ function renderRecipes(tabElement, recipes, options) {
|
||||
const statusBadge = document.createElement('span');
|
||||
statusBadge.className = `recipe-card__badge recipe-card__badge--${statusClass}`;
|
||||
|
||||
// Icon switches by state so status never relies on color alone.
|
||||
const statusIcons = {
|
||||
ready: 'fa-check',
|
||||
missing: 'fa-exclamation-triangle',
|
||||
partial: 'fa-circle-minus',
|
||||
unavailable: 'fa-ban',
|
||||
empty: 'fa-layer-group',
|
||||
};
|
||||
const statusIcon = document.createElement('i');
|
||||
statusIcon.className = 'fas fa-layer-group';
|
||||
statusIcon.className = `fas ${statusIcons[statusClass] || 'fa-layer-group'}`;
|
||||
statusIcon.setAttribute('aria-hidden', 'true');
|
||||
statusBadge.appendChild(statusIcon);
|
||||
|
||||
@@ -216,7 +236,7 @@ function renderRecipes(tabElement, recipes, options) {
|
||||
statusText.textContent = statusLabel;
|
||||
statusBadge.appendChild(statusText);
|
||||
|
||||
statusBadge.title = getLoraStatusTitle(lorasCount, missingLorasCount);
|
||||
statusBadge.title = getLoraStatusTitle(lorasCount, availableLorasCount, missingLorasCount, unavailableLorasCount);
|
||||
meta.appendChild(statusBadge);
|
||||
|
||||
body.appendChild(meta);
|
||||
@@ -264,13 +284,23 @@ function renderRecipes(tabElement, recipes, options) {
|
||||
/**
|
||||
* Returns a descriptive title for the LoRA status indicator
|
||||
* @param {number} totalCount - Total number of LoRAs in recipe
|
||||
* @param {number} missingCount - Number of missing LoRAs
|
||||
* @param {number} availableCount - Number of LoRAs present in the library
|
||||
* @param {number} missingCount - Number of missing LoRAs (still downloadable)
|
||||
* @param {number} unavailableCount - Number of unobtainable LoRAs (deleted
|
||||
* from the source or unresolvable hash)
|
||||
* @returns {string} Status title text
|
||||
*/
|
||||
function getLoraStatusTitle(totalCount, missingCount) {
|
||||
function getLoraStatusTitle(totalCount, availableCount, missingCount, unavailableCount) {
|
||||
if (totalCount === 0) return "No LoRAs in this recipe";
|
||||
if (missingCount === 0) return "All LoRAs available - Ready to use";
|
||||
return `${missingCount} of ${totalCount} LoRAs missing`;
|
||||
if (availableCount === totalCount) return "All LoRAs available - Ready to use";
|
||||
if (missingCount > 0 && unavailableCount > 0) {
|
||||
return `${missingCount} of ${totalCount} LoRAs missing, ${unavailableCount} unavailable (deleted from source or unresolvable hash)`;
|
||||
}
|
||||
if (missingCount > 0) return `${missingCount} of ${totalCount} LoRAs missing`;
|
||||
if (availableCount > 0) {
|
||||
return `${unavailableCount} of ${totalCount} LoRAs unavailable (deleted from source or unresolvable hash) - skipped when recipe is used`;
|
||||
}
|
||||
return `No usable LoRAs - ${unavailableCount} of ${totalCount} deleted from source or unresolvable hash`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user