From f04af2de21e2b815ad0ff155ed019dce3586417e Mon Sep 17 00:00:00 2001
From: Will Miao <13051207myq@gmail.com>
Date: Sun, 30 Mar 2025 19:45:03 +0800
Subject: [PATCH] Add Civitai model retrieval and missing LoRAs download
functionality
- Introduced new API endpoints for fetching Civitai model details by model version ID or hash.
- Enhanced the download manager to support downloading LoRAs using model version ID or hash, improving flexibility.
- Updated RecipeModal to handle missing LoRAs, allowing users to download them directly from the recipe interface.
- Added tooltip and click functionality for missing LoRAs status, enhancing user experience.
- Improved error handling for missing LoRAs download process, providing clearer feedback to users.
---
py/routes/api_routes.py | 35 +-
py/services/download_manager.py | 25 +-
static/css/components/recipe-modal.css | 43 +++
static/js/components/RecipeModal.js | 131 +++++++-
static/js/managers/ImportManager.js | 425 ++++++++++++++-----------
5 files changed, 462 insertions(+), 197 deletions(-)
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';
+ }
+ }
}