diff --git a/py/routes/api_routes.py b/py/routes/api_routes.py index 6d763406..13313402 100644 --- a/py/routes/api_routes.py +++ b/py/routes/api_routes.py @@ -42,6 +42,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_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) @@ -566,6 +568,23 @@ class ApiRoutes: except Exception as e: 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""" + try: + model_version_id = request.match_info['modelVersionId'] + if not model_version_id: + hash = request.match_info['hash'] + model = await self.civitai_client.get_model_by_hash(hash) + return web.json_response(model) + + # Get model details from Civitai API + model = await self.civitai_client.get_model_version_info(model_version_id) + 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)) + async def download_lora(self, request: web.Request) -> web.Response: async with self._download_lock: @@ -579,8 +598,22 @@ class ApiRoutes: 'progress': progress }) + # Check which identifier is provided + download_url = data.get('download_url') + model_hash = data.get('model_hash') + model_version_id = data.get('model_version_id') + + # Validate that at least one identifier is provided + if not any([download_url, model_hash, model_version_id]): + return web.Response( + status=400, + text="Missing required parameter: Please provide either 'download_url', 'hash', or 'modelVersionId'" + ) + result = await self.download_manager.download_from_civitai( - download_url=data.get('download_url'), + download_url=download_url, + model_hash=model_hash, + model_version_id=model_version_id, save_dir=data.get('lora_root'), relative_path=data.get('relative_path'), progress_callback=progress_callback diff --git a/py/services/download_manager.py b/py/services/download_manager.py index add2ce86..fddea0b5 100644 --- a/py/services/download_manager.py +++ b/py/services/download_manager.py @@ -13,8 +13,9 @@ class DownloadManager: self.civitai_client = CivitaiClient() self.file_monitor = file_monitor - async def download_from_civitai(self, download_url: str, save_dir: str, relative_path: str = '', - progress_callback=None) -> Dict: + async def download_from_civitai(self, download_url: str = None, model_hash: str = None, + model_version_id: str = None, save_dir: str = None, + relative_path: str = '', progress_callback=None) -> Dict: try: # Update save directory with relative path if provided if relative_path: @@ -22,9 +23,21 @@ class DownloadManager: # Create directory if it doesn't exist os.makedirs(save_dir, exist_ok=True) - # Get version info - version_id = download_url.split('/')[-1] - version_info = await self.civitai_client.get_model_version_info(version_id) + # Get version info based on the provided identifier + version_info = None + + if download_url: + # Extract version ID from download URL + version_id = download_url.split('/')[-1] + version_info = await self.civitai_client.get_model_version_info(version_id) + elif model_version_id: + # Use model version ID directly + version_info = await self.civitai_client.get_model_version_info(model_version_id) + elif model_hash: + # Get model by hash + version_info = await self.civitai_client.get_model_by_hash(model_hash) + + if not version_info: return {'success': False, 'error': 'Failed to fetch model metadata'} @@ -89,7 +102,7 @@ class DownloadManager: # 6. 开始下载流程 result = await self._execute_download( - download_url=download_url, + download_url=file_info.get('downloadUrl', ''), save_dir=save_dir, metadata=metadata, version_info=version_info, diff --git a/static/css/components/recipe-modal.css b/static/css/components/recipe-modal.css index 27f16b4f..56a5a0ca 100644 --- a/static/css/components/recipe-modal.css +++ b/static/css/components/recipe-modal.css @@ -655,3 +655,46 @@ position: fixed; /* Keep as fixed for Chrome */ z-index: 100; } + +/* Add styles for missing LoRAs download feature */ +.recipe-status.missing { + position: relative; + cursor: pointer; + transition: background-color 0.2s ease; +} + +.recipe-status.missing:hover { + background-color: rgba(var(--lora-warning-rgb, 255, 165, 0), 0.2); +} + +.recipe-status.missing .missing-tooltip { + position: absolute; + display: none; + background-color: var(--card-bg); + color: var(--text-color); + padding: 8px 12px; + border-radius: var(--border-radius-xs); + border: 1px solid var(--border-color); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + z-index: var(--z-overlay); + width: max-content; + max-width: 200px; + font-size: 0.85rem; + font-weight: normal; + margin-left: -100px; + margin-top: -65px; +} + +.recipe-status.missing:hover .missing-tooltip { + display: block; +} + +.recipe-status.clickable { + cursor: pointer; + padding: 4px 8px; + border-radius: var(--border-radius-xs); +} + +.recipe-status.clickable:hover { + background-color: rgba(var(--lora-warning-rgb, 255, 165, 0), 0.2); +} diff --git a/static/js/components/RecipeModal.js b/static/js/components/RecipeModal.js index b4d4b7cc..4ced48f4 100644 --- a/static/js/components/RecipeModal.js +++ b/static/js/components/RecipeModal.js @@ -1,5 +1,6 @@ // Recipe Modal Component import { showToast } from '../utils/uiHelpers.js'; +import { state } from '../state/index.js'; class RecipeModal { constructor() { @@ -50,6 +51,21 @@ class RecipeModal { tooltip.style.left = (badgeRect.right - tooltip.offsetWidth) + 'px'; } } + + // Add tooltip positioning for missing badge + if (event.target.closest('.recipe-status.missing')) { + const badge = event.target.closest('.recipe-status.missing'); + const tooltip = badge.querySelector('.missing-tooltip'); + + if (tooltip) { + // Get badge position + const badgeRect = badge.getBoundingClientRect(); + + // Position the tooltip + tooltip.style.top = (badgeRect.bottom + 4) + 'px'; + tooltip.style.left = (badgeRect.left) + 'px'; + } + } }, true); } @@ -304,7 +320,10 @@ class RecipeModal { statusHTML = `
Ready to use
`; } else if (missingLorasCount > 0) { // Some LoRAs are missing (prioritize showing missing over deleted) - statusHTML = `
${missingLorasCount} missing
`; + statusHTML = `
+ ${missingLorasCount} missing +
Click to download missing LoRAs
+
`; } else if (deletedLorasCount > 0 && missingLorasCount === 0) { // Some LoRAs are deleted but none are missing statusHTML = `
${deletedLorasCount} deleted
`; @@ -312,6 +331,15 @@ class RecipeModal { } lorasCountElement.innerHTML = ` ${totalCount} LoRAs ${statusHTML}`; + + // Add click handler for missing LoRAs status + setTimeout(() => { + const missingStatus = document.querySelector('.recipe-status.missing'); + if (missingStatus && missingLorasCount > 0) { + missingStatus.classList.add('clickable'); + missingStatus.addEventListener('click', () => this.showDownloadMissingLorasModal()); + } + }, 100); } if (lorasListElement && recipe.loras && recipe.loras.length > 0) { @@ -385,6 +413,8 @@ class RecipeModal { lorasListElement.innerHTML = '
No LoRAs associated with this recipe
'; this.recipeLorasSyntax = ''; } + + console.log(this.currentRecipe.loras); // Show the modal modalManager.showModal('recipeModal'); @@ -700,6 +730,105 @@ class RecipeModal { showToast('Failed to copy text', 'error'); }); } + + // Add new method to handle downloading missing LoRAs + async showDownloadMissingLorasModal() { + console.log("currentRecipe", this.currentRecipe); + // Get missing LoRAs from the current recipe + const missingLoras = this.currentRecipe.loras.filter(lora => !lora.inLibrary); + console.log("missingLoras", missingLoras); + + if (missingLoras.length === 0) { + showToast('No missing LoRAs to download', 'info'); + return; + } + + try { + state.loadingManager.showSimpleLoading('Getting version info for missing LoRAs...'); + + // Get version info for each missing LoRA by calling the appropriate API endpoint + const missingLorasWithVersionInfoPromises = missingLoras.map(async lora => { + let endpoint; + + // Determine which endpoint to use based on available data + if (lora.modelVersionId) { + endpoint = `/api/civitai/model/${lora.modelVersionId}`; + } else if (lora.hash) { + endpoint = `/api/civitai/model/${lora.hash}`; + } else { + console.error("Missing both hash and modelVersionId for lora:", lora); + return null; + } + + const response = await fetch(endpoint); + const versionInfo = await response.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); + console.log("Loras with version info:", lorasWithVersionInfo); + + // 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; + } + + // Close the recipe modal first + modalManager.closeModal('recipeModal'); + + // 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 || '' + }; + }) + }; + + console.log("recipeData for import:", recipeData); + + // Call ImportManager's download missing LoRAs method + window.importManager.downloadMissingLoras(recipeData, this.currentRecipe.id); + } catch (error) { + console.error("Error downloading missing LoRAs:", error); + showToast('Error preparing LoRAs for download', 'error'); + } finally { + state.loadingManager.hide(); + } + } } export { RecipeModal }; \ No newline at end of file diff --git a/static/js/managers/ImportManager.js b/static/js/managers/ImportManager.js index cfbb5c33..0aa8ddd9 100644 --- a/static/js/managers/ImportManager.js +++ b/static/js/managers/ImportManager.js @@ -26,7 +26,7 @@ export class ImportManager { this.importMode = 'upload'; // Default mode: 'upload' or 'url' } - showImportModal() { + showImportModal(recipeData = null, recipeId = null) { if (!this.initialized) { // Check if modal exists const modal = document.getElementById('importModal'); @@ -39,6 +39,10 @@ export class ImportManager { // Always reset the state when opening the modal this.resetSteps(); + if (recipeData) { + this.downloadableLoRAs = recipeData.loras; + this.recipeId = recipeId; + } // Show the modal modalManager.showModal('importModal', null, () => { @@ -831,219 +835,233 @@ export class ImportManager { } async saveRecipe() { - if (!this.recipeName) { + // Check if we're in download-only mode (for existing recipe) + const isDownloadOnly = !!this.recipeId; + + console.log("isDownloadOnly", isDownloadOnly); + + if (!isDownloadOnly && !this.recipeName) { showToast('Please enter a recipe name', 'error'); return; } try { - // First save the recipe - this.loadingManager.showSimpleLoading('Saving recipe...'); + this.loadingManager.showSimpleLoading(isDownloadOnly ? 'Preparing download...' : 'Saving recipe...'); - // Create form data for save request - const formData = new FormData(); - - // Handle image data - either from file upload or from URL mode - if (this.recipeImage) { - // File upload mode - formData.append('image', this.recipeImage); - } else if (this.recipeData && this.recipeData.image_base64) { - // URL mode with base64 data - formData.append('image_base64', this.recipeData.image_base64); - } else if (this.importMode === 'url') { - // Fallback for URL mode - tell backend to fetch the image again - const urlInput = document.getElementById('imageUrlInput'); - if (urlInput && urlInput.value) { - formData.append('image_url', urlInput.value); + // If we're only downloading LoRAs for an existing recipe, skip the recipe save step + if (!isDownloadOnly) { + // First save the recipe + // Create form data for save request + const formData = new FormData(); + + // Handle image data - either from file upload or from URL mode + if (this.recipeImage) { + // File upload mode + formData.append('image', this.recipeImage); + } else if (this.recipeData && this.recipeData.image_base64) { + // URL mode with base64 data + formData.append('image_base64', this.recipeData.image_base64); + } else if (this.importMode === 'url') { + // Fallback for URL mode - tell backend to fetch the image again + const urlInput = document.getElementById('imageUrlInput'); + if (urlInput && urlInput.value) { + formData.append('image_url', urlInput.value); + } else { + throw new Error('No image data available'); + } } else { throw new Error('No image data available'); } - } else { - throw new Error('No image data available'); + + formData.append('name', this.recipeName); + formData.append('tags', JSON.stringify(this.recipeTags)); + + // Prepare complete metadata including generation parameters + const completeMetadata = { + base_model: this.recipeData.base_model || "", + loras: this.recipeData.loras || [], + gen_params: this.recipeData.gen_params || {}, + raw_metadata: this.recipeData.raw_metadata || {} + }; + + formData.append('metadata', JSON.stringify(completeMetadata)); + + // Send save request + const response = await fetch('/api/recipes/save', { + method: 'POST', + body: formData + }); + + const result = await response.json(); + + if (!result.success) { + // Handle save error + console.error("Failed to save recipe:", result.error); + showToast(result.error, 'error'); + // Close modal + modalManager.closeModal('importModal'); + return; + } } - - formData.append('name', this.recipeName); - formData.append('tags', JSON.stringify(this.recipeTags)); - - // Prepare complete metadata including generation parameters - const completeMetadata = { - base_model: this.recipeData.base_model || "", - loras: this.recipeData.loras || [], - gen_params: this.recipeData.gen_params || {}, - raw_metadata: this.recipeData.raw_metadata || {} - }; - - formData.append('metadata', JSON.stringify(completeMetadata)); - - // Send save request - const response = await fetch('/api/recipes/save', { - method: 'POST', - body: formData - }); - - const result = await response.json(); - if (result.success) { - // Handle successful save + + // Check if we need to download LoRAs + if (this.downloadableLoRAs && this.downloadableLoRAs.length > 0) { + // For download, we need to validate the target path + const loraRoot = document.getElementById('importLoraRoot')?.value; + if (!loraRoot) { + throw new Error('Please select a LoRA root directory'); + } + // Build target path + let targetPath = loraRoot; + if (this.selectedFolder) { + targetPath += '/' + this.selectedFolder; + } - // Check if we need to download LoRAs - if (this.downloadableLoRAs && this.downloadableLoRAs.length > 0) { - // For download, we need to validate the target path - const loraRoot = document.getElementById('importLoraRoot')?.value; - if (!loraRoot) { - throw new Error('Please select a LoRA root directory'); - } - - // Build target path - let targetPath = loraRoot; - if (this.selectedFolder) { - targetPath += '/' + this.selectedFolder; - } - - const newFolder = document.getElementById('importNewFolder')?.value?.trim(); - if (newFolder) { - targetPath += '/' + newFolder; - } - - // Set up WebSocket for progress updates - const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://'; - const ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/fetch-progress`); - - // Show enhanced loading with progress details for multiple items - const updateProgress = this.loadingManager.showDownloadProgress(this.downloadableLoRAs.length); - - let completedDownloads = 0; - let failedDownloads = 0; - let earlyAccessFailures = 0; - let currentLoraProgress = 0; - - // Set up progress tracking for current download - ws.onmessage = (event) => { - const data = JSON.parse(event.data); - if (data.status === 'progress') { - // Update current LoRA progress - currentLoraProgress = data.progress; - - // Get current LoRA name - const currentLora = this.downloadableLoRAs[completedDownloads + failedDownloads]; - const loraName = currentLora ? currentLora.name : ''; - - // Update progress display - updateProgress(currentLoraProgress, completedDownloads, loraName); - - // Add more detailed status messages based on progress - if (currentLoraProgress < 3) { - this.loadingManager.setStatus( - `Preparing download for LoRA ${completedDownloads + failedDownloads + 1}/${this.downloadableLoRAs.length}` - ); - } else if (currentLoraProgress === 3) { - this.loadingManager.setStatus( - `Downloaded preview for LoRA ${completedDownloads + failedDownloads + 1}/${this.downloadableLoRAs.length}` - ); - } else if (currentLoraProgress > 3 && currentLoraProgress < 100) { - this.loadingManager.setStatus( - `Downloading LoRA ${completedDownloads + failedDownloads + 1}/${this.downloadableLoRAs.length}` - ); - } else { - this.loadingManager.setStatus( - `Finalizing LoRA ${completedDownloads + failedDownloads + 1}/${this.downloadableLoRAs.length}` - ); - } - } - }; - - for (let i = 0; i < this.downloadableLoRAs.length; i++) { - const lora = this.downloadableLoRAs[i]; + const newFolder = document.getElementById('importNewFolder')?.value?.trim(); + if (newFolder) { + targetPath += '/' + newFolder; + } + + // Set up WebSocket for progress updates + const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://'; + const ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/fetch-progress`); + + // Show enhanced loading with progress details for multiple items + const updateProgress = this.loadingManager.showDownloadProgress(this.downloadableLoRAs.length); + + let completedDownloads = 0; + let failedDownloads = 0; + let earlyAccessFailures = 0; + let currentLoraProgress = 0; + + // Set up progress tracking for current download + ws.onmessage = (event) => { + const data = JSON.parse(event.data); + if (data.status === 'progress') { + // Update current LoRA progress + currentLoraProgress = data.progress; - // Reset current LoRA progress for new download - currentLoraProgress = 0; + // Get current LoRA name + const currentLora = this.downloadableLoRAs[completedDownloads + failedDownloads]; + const loraName = currentLora ? currentLora.name : ''; - // Initial status update for new LoRA - this.loadingManager.setStatus(`Starting download for LoRA ${i+1}/${this.downloadableLoRAs.length}`); - updateProgress(0, completedDownloads, lora.name); + // Update progress display + updateProgress(currentLoraProgress, completedDownloads, loraName); - try { - // Download the LoRA - const response = await fetch('/api/download-lora', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - download_url: lora.downloadUrl, - lora_root: loraRoot, - relative_path: targetPath.replace(loraRoot + '/', '') - }) - }); - - if (!response.ok) { - const errorText = await response.text(); - console.error(`Failed to download LoRA ${lora.name}: ${errorText}`); - - // Check if this is an early access error (status 401 is the key indicator) - if (response.status === 401 || - (errorText.toLowerCase().includes('early access') || - errorText.toLowerCase().includes('purchase'))) { - earlyAccessFailures++; - this.loadingManager.setStatus( - `Failed to download ${lora.name}: Early Access required` - ); - } - - failedDownloads++; - // Continue with next download - } else { - completedDownloads++; - - // Update progress to show completion of current LoRA - updateProgress(100, completedDownloads, ''); - - if (completedDownloads + failedDownloads < this.downloadableLoRAs.length) { - this.loadingManager.setStatus( - `Completed ${completedDownloads}/${this.downloadableLoRAs.length} LoRAs. Starting next download...` - ); - } - } - } catch (downloadError) { - console.error(`Error downloading LoRA ${lora.name}:`, downloadError); - failedDownloads++; - // Continue with next download - } - } - - // Close WebSocket - ws.close(); - - // Show appropriate completion message based on results - if (failedDownloads === 0) { - showToast(`All ${completedDownloads} LoRAs downloaded successfully`, 'success'); - } else { - if (earlyAccessFailures > 0) { - showToast( - `Downloaded ${completedDownloads} of ${this.downloadableLoRAs.length} LoRAs. ${earlyAccessFailures} failed due to Early Access restrictions.`, - 'error' + // Add more detailed status messages based on progress + if (currentLoraProgress < 3) { + this.loadingManager.setStatus( + `Preparing download for LoRA ${completedDownloads + failedDownloads + 1}/${this.downloadableLoRAs.length}` + ); + } else if (currentLoraProgress === 3) { + this.loadingManager.setStatus( + `Downloaded preview for LoRA ${completedDownloads + failedDownloads + 1}/${this.downloadableLoRAs.length}` + ); + } else if (currentLoraProgress > 3 && currentLoraProgress < 100) { + this.loadingManager.setStatus( + `Downloading LoRA ${completedDownloads + failedDownloads + 1}/${this.downloadableLoRAs.length}` ); } else { - showToast(`Downloaded ${completedDownloads} of ${this.downloadableLoRAs.length} LoRAs`, 'error'); + this.loadingManager.setStatus( + `Finalizing LoRA ${completedDownloads + failedDownloads + 1}/${this.downloadableLoRAs.length}` + ); } } + }; + + for (let i = 0; i < this.downloadableLoRAs.length; i++) { + const lora = this.downloadableLoRAs[i]; + + // Reset current LoRA progress for new download + currentLoraProgress = 0; + + // Initial status update for new LoRA + this.loadingManager.setStatus(`Starting download for LoRA ${i+1}/${this.downloadableLoRAs.length}`); + updateProgress(0, completedDownloads, lora.name); + + try { + // Download the LoRA + const response = await fetch('/api/download-lora', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + download_url: lora.downloadUrl, + model_version_id: lora.modelVersionId, + model_hash: lora.hash, + lora_root: loraRoot, + relative_path: targetPath.replace(loraRoot + '/', '') + }) + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error(`Failed to download LoRA ${lora.name}: ${errorText}`); + + // Check if this is an early access error (status 401 is the key indicator) + if (response.status === 401 || + (errorText.toLowerCase().includes('early access') || + errorText.toLowerCase().includes('purchase'))) { + earlyAccessFailures++; + this.loadingManager.setStatus( + `Failed to download ${lora.name}: Early Access required` + ); + } + + failedDownloads++; + // Continue with next download + } else { + completedDownloads++; + + // Update progress to show completion of current LoRA + updateProgress(100, completedDownloads, ''); + + if (completedDownloads + failedDownloads < this.downloadableLoRAs.length) { + this.loadingManager.setStatus( + `Completed ${completedDownloads}/${this.downloadableLoRAs.length} LoRAs. Starting next download...` + ); + } + } + } catch (downloadError) { + console.error(`Error downloading LoRA ${lora.name}:`, downloadError); + failedDownloads++; + // Continue with next download + } } + + // Close WebSocket + ws.close(); + + // Show appropriate completion message based on results + if (failedDownloads === 0) { + showToast(`All ${completedDownloads} LoRAs downloaded successfully`, 'success'); + } else { + if (earlyAccessFailures > 0) { + showToast( + `Downloaded ${completedDownloads} of ${this.downloadableLoRAs.length} LoRAs. ${earlyAccessFailures} failed due to Early Access restrictions.`, + 'error' + ); + } else { + showToast(`Downloaded ${completedDownloads} of ${this.downloadableLoRAs.length} LoRAs`, 'error'); + } + } + } - // Show success message for recipe save - showToast(`Recipe "${this.recipeName}" saved successfully`, 'success'); - - // Close modal and reload recipes - modalManager.closeModal('importModal'); - - window.recipeManager.loadRecipes(true); // true to reset pagination - + // Show success message + if (isDownloadOnly) { + showToast('LoRAs downloaded successfully', 'success'); } else { - // Handle error - console.error(`Failed to save recipe: ${result.error}`); - // Show error message to user - showToast(result.error, 'error'); + showToast(`Recipe "${this.recipeName}" saved successfully`, 'success'); } + // Close modal + modalManager.closeModal('importModal'); + + // Refresh the recipe + window.recipeManager.loadRecipes(this.recipeId); + } catch (error) { - console.error('Error saving recipe:', error); + console.error('Error:', error); showToast(error.message, 'error'); } finally { this.loadingManager.hide(); @@ -1205,4 +1223,33 @@ export class ImportManager { return true; } + + // Add new method to handle downloading missing LoRAs from a recipe + downloadMissingLoras(recipeData, recipeId) { + // Store the recipe data and ID + this.recipeData = recipeData; + this.recipeId = recipeId; + + // Show the location step directly + this.showImportModal(recipeData, recipeId); + this.proceedToLocation(); + + // Update the modal title to reflect we're downloading for an existing recipe + const modalTitle = document.querySelector('#importModal h2'); + if (modalTitle) { + modalTitle.textContent = 'Download Missing LoRAs'; + } + + // Update the save button text + const saveButton = document.querySelector('#locationStep .primary-btn'); + if (saveButton) { + saveButton.textContent = 'Download Missing LoRAs'; + } + + // Hide the back button since we're skipping steps + const backButton = document.querySelector('#locationStep .secondary-btn'); + if (backButton) { + backButton.style.display = 'none'; + } + } }