diff --git a/py/routes/api_routes.py b/py/routes/api_routes.py index 6c67e129..39d0d429 100644 --- a/py/routes/api_routes.py +++ b/py/routes/api_routes.py @@ -50,8 +50,8 @@ class ApiRoutes: app.router.add_get('/api/lora-roots', routes.get_lora_roots) app.router.add_get('/api/folders', routes.get_folders) app.router.add_get('/api/civitai/versions/{model_id}', routes.get_civitai_versions) - app.router.add_get('/api/civitai/model/{modelVersionId}', routes.get_civitai_model) - app.router.add_get('/api/civitai/model/{hash}', routes.get_civitai_model) + app.router.add_get('/api/civitai/model/version/{modelVersionId}', routes.get_civitai_model_by_version) + app.router.add_get('/api/civitai/model/hash/{hash}', routes.get_civitai_model_by_hash) app.router.add_post('/api/download-lora', routes.download_lora) app.router.add_post('/api/settings', routes.update_settings) app.router.add_post('/api/move_model', routes.move_model) @@ -396,30 +396,52 @@ class ApiRoutes: logger.error(f"Error fetching model versions: {e}") return web.Response(status=500, text=str(e)) - async def get_civitai_model(self, request: web.Request) -> web.Response: - """Get CivitAI model details by model version ID or hash""" + async def get_civitai_model_by_version(self, request: web.Request) -> web.Response: + """Get CivitAI model details by model version ID""" try: if self.civitai_client is None: self.civitai_client = await ServiceRegistry.get_civitai_client() model_version_id = request.match_info.get('modelVersionId') - if not model_version_id: - hash = request.match_info.get('hash') - model = await self.civitai_client.get_model_by_hash(hash) - return web.json_response(model) # Get model details from Civitai API model, error_msg = await self.civitai_client.get_model_version_info(model_version_id) if not model: - status_code = 404 if error_msg and "model not found" in error_msg.lower() else 500 - return web.Response(status=status_code, text=error_msg or "Failed to fetch model information") + # Log warning for failed model retrieval + logger.warning(f"Failed to fetch model version {model_version_id}: {error_msg}") + + # Determine status code based on error message + status_code = 404 if error_msg and "not found" in error_msg.lower() else 500 + + return web.json_response({ + "success": False, + "error": error_msg or "Failed to fetch model information" + }, status=status_code) return web.json_response(model) except Exception as e: logger.error(f"Error fetching model details: {e}") - return web.Response(status=500, text=str(e)) - + return web.json_response({ + "success": False, + "error": str(e) + }, status=500) + + async def get_civitai_model_by_hash(self, request: web.Request) -> web.Response: + """Get CivitAI model details by hash""" + try: + if self.civitai_client is None: + self.civitai_client = await ServiceRegistry.get_civitai_client() + + hash = request.match_info.get('hash') + model = await self.civitai_client.get_model_by_hash(hash) + return web.json_response(model) + except Exception as e: + logger.error(f"Error fetching model details by hash: {e}") + return web.json_response({ + "success": False, + "error": str(e) + }, status=500) async def download_lora(self, request: web.Request) -> web.Response: async with self._download_lock: diff --git a/static/js/components/ContextMenu/RecipeContextMenu.js b/static/js/components/ContextMenu/RecipeContextMenu.js index ca5c54d8..77145f65 100644 --- a/static/js/components/ContextMenu/RecipeContextMenu.js +++ b/static/js/components/ContextMenu/RecipeContextMenu.js @@ -1,12 +1,38 @@ import { BaseContextMenu } from './BaseContextMenu.js'; import { showToast } from '../../utils/uiHelpers.js'; +import { setSessionItem, removeSessionItem } from '../../utils/storageHelpers.js'; +import { state } from '../../state/index.js'; export class RecipeContextMenu extends BaseContextMenu { constructor() { super('recipeContextMenu', '.lora-card'); } + showMenu(x, y, card) { + // Call the parent method first to handle basic positioning + super.showMenu(x, y, card); + + // Get recipe data to check for missing LoRAs + const recipeId = card.dataset.id; + const missingLorasItem = this.menu.querySelector('.download-missing-item'); + + if (recipeId && missingLorasItem) { + // Check if this card has missing LoRAs + const loraCountElement = card.querySelector('.lora-count'); + const hasMissingLoras = loraCountElement && loraCountElement.classList.contains('missing'); + + // Show/hide the download missing LoRAs option based on missing status + if (hasMissingLoras) { + missingLorasItem.style.display = 'flex'; + } else { + missingLorasItem.style.display = 'none'; + } + } + } + handleMenuAction(action) { + const recipeId = this.currentCard.dataset.id; + switch(action) { case 'details': // Show recipe details @@ -14,28 +40,166 @@ export class RecipeContextMenu extends BaseContextMenu { break; case 'copy': // Copy recipe to clipboard - if (window.recipeManager) { - window.recipeManager.copyRecipe(this.currentCard.dataset.id); - } + this.currentCard.querySelector('.fa-copy')?.click(); break; case 'share': // Share recipe - if (window.recipeManager) { - window.recipeManager.shareRecipe(this.currentCard.dataset.id); - } + this.currentCard.querySelector('.fa-share-alt')?.click(); break; case 'delete': // Delete recipe - if (this.currentCard.querySelector('.fa-trash')) { - this.currentCard.querySelector('.fa-trash').click(); - } + this.currentCard.querySelector('.fa-trash')?.click(); break; - case 'edit': - // Edit recipe - if (window.recipeManager && window.recipeManager.editRecipe) { - window.recipeManager.editRecipe(this.currentCard.dataset.id); - } + case 'viewloras': + // View all LoRAs in the recipe + this.viewRecipeLoRAs(recipeId); + break; + case 'download-missing': + // Download missing LoRAs + this.downloadMissingLoRAs(recipeId); break; } } + + // View all LoRAs in the recipe + viewRecipeLoRAs(recipeId) { + if (!recipeId) { + showToast('Cannot view LoRAs: Missing recipe ID', 'error'); + return; + } + + // First get the recipe details to access its LoRAs + fetch(`/api/recipe/${recipeId}`) + .then(response => response.json()) + .then(recipe => { + // Clear any previous filters first + removeSessionItem('recipe_to_lora_filterLoraHash'); + removeSessionItem('recipe_to_lora_filterLoraHashes'); + removeSessionItem('filterRecipeName'); + removeSessionItem('viewLoraDetail'); + + // Collect all hashes from the recipe's LoRAs + const loraHashes = recipe.loras + .filter(lora => lora.hash) + .map(lora => lora.hash.toLowerCase()); + + if (loraHashes.length > 0) { + // Store the LoRA hashes and recipe name in session storage + setSessionItem('recipe_to_lora_filterLoraHashes', JSON.stringify(loraHashes)); + setSessionItem('filterRecipeName', recipe.title); + + // Navigate to the LoRAs page + window.location.href = '/loras'; + } else { + showToast('No LoRAs found in this recipe', 'info'); + } + }) + .catch(error => { + console.error('Error loading recipe LoRAs:', error); + showToast('Error loading recipe LoRAs: ' + error.message, 'error'); + }); + } + + // Download missing LoRAs + async downloadMissingLoRAs(recipeId) { + if (!recipeId) { + showToast('Cannot download LoRAs: Missing recipe ID', 'error'); + return; + } + + try { + // First get the recipe details + const response = await fetch(`/api/recipe/${recipeId}`); + const recipe = await response.json(); + + // Get missing LoRAs + const missingLoras = recipe.loras.filter(lora => !lora.inLibrary && !lora.isDeleted); + + if (missingLoras.length === 0) { + showToast('No missing LoRAs to download', 'info'); + return; + } + + // Show loading toast + state.loadingManager.showSimpleLoading('Getting version info for missing LoRAs...'); + + // Get version info for each missing LoRA + const missingLorasWithVersionInfoPromises = missingLoras.map(async lora => { + let endpoint; + + // Determine which endpoint to use based on available data + if (lora.modelVersionId) { + endpoint = `/api/civitai/model/version/${lora.modelVersionId}`; + } else if (lora.hash) { + endpoint = `/api/civitai/model/hash/${lora.hash}`; + } else { + console.error("Missing both hash and modelVersionId for lora:", lora); + return null; + } + + const versionResponse = await fetch(endpoint); + const versionInfo = await versionResponse.json(); + + // Return original lora data combined with version info + return { + ...lora, + civitaiInfo: versionInfo + }; + }); + + // Wait for all API calls to complete + const lorasWithVersionInfo = await Promise.all(missingLorasWithVersionInfoPromises); + + // Filter out null values (failed requests) + const validLoras = lorasWithVersionInfo.filter(lora => lora !== null); + + if (validLoras.length === 0) { + showToast('Failed to get information for missing LoRAs', 'error'); + return; + } + + // Prepare data for import manager using the retrieved information + const recipeData = { + loras: validLoras.map(lora => { + const civitaiInfo = lora.civitaiInfo; + const modelFile = civitaiInfo.files ? + civitaiInfo.files.find(file => file.type === 'Model') : null; + + return { + // Basic lora info + name: civitaiInfo.model?.name || lora.name, + version: civitaiInfo.name || '', + strength: lora.strength || 1.0, + + // Model identifiers + hash: modelFile?.hashes?.SHA256?.toLowerCase() || lora.hash, + modelVersionId: civitaiInfo.id || lora.modelVersionId, + + // Metadata + thumbnailUrl: civitaiInfo.images?.[0]?.url || '', + baseModel: civitaiInfo.baseModel || '', + downloadUrl: civitaiInfo.downloadUrl || '', + size: modelFile ? (modelFile.sizeKB * 1024) : 0, + file_name: modelFile ? modelFile.name.split('.')[0] : '', + + // Status flags + existsLocally: false, + isDeleted: civitaiInfo.error === "Model not found", + isEarlyAccess: !!civitaiInfo.earlyAccessEndsAt, + earlyAccessEndsAt: civitaiInfo.earlyAccessEndsAt || '' + }; + }) + }; + + // Call ImportManager's download missing LoRAs method + window.importManager.downloadMissingLoras(recipeData, recipeId); + } catch (error) { + console.error('Error downloading missing LoRAs:', error); + showToast('Error preparing LoRAs for download: ' + error.message, 'error'); + } finally { + if (state.loadingManager) { + state.loadingManager.hide(); + } + } + } } \ No newline at end of file diff --git a/static/js/components/RecipeModal.js b/static/js/components/RecipeModal.js index e0f52af5..ec09ec0c 100644 --- a/static/js/components/RecipeModal.js +++ b/static/js/components/RecipeModal.js @@ -790,9 +790,9 @@ class RecipeModal { // Determine which endpoint to use based on available data if (lora.modelVersionId) { - endpoint = `/api/civitai/model/${lora.modelVersionId}`; + endpoint = `/api/civitai/model/version/${lora.modelVersionId}`; } else if (lora.hash) { - endpoint = `/api/civitai/model/${lora.hash}`; + endpoint = `/api/civitai/model/hash/${lora.hash}`; } else { console.error("Missing both hash and modelVersionId for lora:", lora); return null; diff --git a/templates/recipes.html b/templates/recipes.html index c0202876..4234fd77 100644 --- a/templates/recipes.html +++ b/templates/recipes.html @@ -19,11 +19,12 @@
{% endblock %}