import { modalManager } from './ModalManager.js'; import { showToast } from '../utils/uiHelpers.js'; import { LoadingManager } from './LoadingManager.js'; import { getStorageItem } from '../utils/storageHelpers.js'; export class ImportManager { constructor() { this.recipeImage = null; this.recipeData = null; this.recipeName = ''; this.recipeTags = []; this.missingLoras = []; // Add initialization check this.initialized = false; this.selectedFolder = ''; // Add LoadingManager instance this.loadingManager = new LoadingManager(); this.folderClickHandler = null; this.updateTargetPath = this.updateTargetPath.bind(this); // 添加对注入样式的引用 this.injectedStyles = null; // Add import mode tracking this.importMode = 'upload'; // Default mode: 'upload' or 'url' } showImportModal(recipeData = null, recipeId = null) { if (!this.initialized) { // Check if modal exists const modal = document.getElementById('importModal'); if (!modal) { console.error('Import modal element not found'); return; } this.initialized = true; } // 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, () => { // Cleanup handler when modal closes this.cleanupFolderBrowser(); // Remove any injected styles this.removeInjectedStyles(); }); // Verify the modal is properly shown setTimeout(() => { this.ensureModalVisible(); }, 50); } // 添加移除注入样式的方法 removeInjectedStyles() { if (this.injectedStyles && this.injectedStyles.parentNode) { this.injectedStyles.parentNode.removeChild(this.injectedStyles); this.injectedStyles = null; } // Also reset any inline styles that might have been set with !important document.querySelectorAll('.import-step').forEach(step => { step.style.cssText = ''; }); } resetSteps() { // Remove any existing injected styles this.removeInjectedStyles(); // Show the first step this.showStep('uploadStep'); // Reset file input const fileInput = document.getElementById('recipeImageUpload'); if (fileInput) { fileInput.value = ''; } // Reset URL input const urlInput = document.getElementById('imageUrlInput'); if (urlInput) { urlInput.value = ''; } // Reset error messages const uploadError = document.getElementById('uploadError'); if (uploadError) { uploadError.textContent = ''; } const urlError = document.getElementById('urlError'); if (urlError) { urlError.textContent = ''; } // Reset recipe name input const recipeName = document.getElementById('recipeName'); if (recipeName) { recipeName.value = ''; } // Reset tags container const tagsContainer = document.getElementById('tagsContainer'); if (tagsContainer) { tagsContainer.innerHTML = '
No tags added
'; } // Reset state variables this.recipeImage = null; this.recipeData = null; this.recipeName = ''; this.recipeTags = []; this.missingLoras = []; this.downloadableLoRAs = []; // Reset import mode to upload this.importMode = 'upload'; this.toggleImportMode('upload'); // Clear selected folder and remove selection from UI this.selectedFolder = ''; const folderBrowser = document.getElementById('importFolderBrowser'); if (folderBrowser) { folderBrowser.querySelectorAll('.folder-item').forEach(f => f.classList.remove('selected')); } // Clear missing LoRAs list if it exists const missingLorasList = document.getElementById('missingLorasList'); if (missingLorasList) { missingLorasList.innerHTML = ''; } // Reset total download size const totalSizeDisplay = document.getElementById('totalDownloadSize'); if (totalSizeDisplay) { totalSizeDisplay.textContent = 'Calculating...'; } // Remove any existing deleted LoRAs warning const deletedLorasWarning = document.getElementById('deletedLorasWarning'); if (deletedLorasWarning) { deletedLorasWarning.remove(); } // Remove any existing early access warning const earlyAccessWarning = document.getElementById('earlyAccessWarning'); if (earlyAccessWarning) { earlyAccessWarning.remove(); } } toggleImportMode(mode) { this.importMode = mode; // Update toggle buttons const uploadBtn = document.querySelector('.toggle-btn[data-mode="upload"]'); const urlBtn = document.querySelector('.toggle-btn[data-mode="url"]'); if (uploadBtn && urlBtn) { if (mode === 'upload') { uploadBtn.classList.add('active'); urlBtn.classList.remove('active'); } else { uploadBtn.classList.remove('active'); urlBtn.classList.add('active'); } } // Show/hide appropriate sections const uploadSection = document.getElementById('uploadSection'); const urlSection = document.getElementById('urlSection'); if (uploadSection && urlSection) { if (mode === 'upload') { uploadSection.style.display = 'block'; urlSection.style.display = 'none'; } else { uploadSection.style.display = 'none'; urlSection.style.display = 'block'; } } // Clear error messages const uploadError = document.getElementById('uploadError'); const urlError = document.getElementById('urlError'); if (uploadError) uploadError.textContent = ''; if (urlError) urlError.textContent = ''; } handleImageUpload(event) { const file = event.target.files[0]; const errorElement = document.getElementById('uploadError'); if (!file) { return; } // Validate file type if (!file.type.match('image.*')) { errorElement.textContent = 'Please select an image file'; return; } // Reset error errorElement.textContent = ''; this.recipeImage = file; // Auto-proceed to next step if file is selected this.uploadAndAnalyzeImage(); } async handleUrlInput() { const urlInput = document.getElementById('imageUrlInput'); const errorElement = document.getElementById('urlError'); const url = urlInput.value.trim(); // Validate URL if (!url) { errorElement.textContent = 'Please enter a URL'; return; } // Basic URL validation if (!url.startsWith('http://') && !url.startsWith('https://')) { errorElement.textContent = 'Please enter a valid URL'; return; } // Reset error errorElement.textContent = ''; // Show loading indicator this.loadingManager.showSimpleLoading('Fetching image from URL...'); try { // Call API to analyze the URL await this.analyzeImageFromUrl(url); } catch (error) { errorElement.textContent = error.message || 'Failed to fetch image from URL'; } finally { this.loadingManager.hide(); } } async analyzeImageFromUrl(url) { try { // Call the API with URL data const response = await fetch('/api/recipes/analyze-image', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: url }) }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || 'Failed to analyze image from URL'); } // Get recipe data from response this.recipeData = await response.json(); // Check if we have an error message if (this.recipeData.error) { throw new Error(this.recipeData.error); } // Check if we have valid recipe data if (!this.recipeData || !this.recipeData.loras || this.recipeData.loras.length === 0) { throw new Error('No LoRA information found in this image'); } // Find missing LoRAs this.missingLoras = this.recipeData.loras.filter(lora => !lora.existsLocally); // Proceed to recipe details step this.showRecipeDetailsStep(); } catch (error) { console.error('Error analyzing URL:', error); throw error; } } async uploadAndAnalyzeImage() { if (!this.recipeImage) { showToast('Please select an image first', 'error'); return; } try { this.loadingManager.showSimpleLoading('Analyzing image metadata...'); // Create form data for upload const formData = new FormData(); formData.append('image', this.recipeImage); // Upload image for analysis const response = await fetch('/api/recipes/analyze-image', { method: 'POST', body: formData }); // Get recipe data from response this.recipeData = await response.json(); console.log('Recipe data:', this.recipeData); // Check if we have an error message if (this.recipeData.error) { throw new Error(this.recipeData.error); } // Check if we have valid recipe data if (!this.recipeData || !this.recipeData.loras || this.recipeData.loras.length === 0) { throw new Error('No LoRA information found in this image'); } // Store generation parameters if available if (this.recipeData.gen_params) { console.log('Generation parameters found:', this.recipeData.gen_params); } // Find missing LoRAs this.missingLoras = this.recipeData.loras.filter(lora => !lora.existsLocally); // Proceed to recipe details step this.showRecipeDetailsStep(); } catch (error) { document.getElementById('uploadError').textContent = error.message; } finally { this.loadingManager.hide(); } } showRecipeDetailsStep() { this.showStep('detailsStep'); // Set default recipe name from prompt or image filename const recipeName = document.getElementById('recipeName'); // Check if we have recipe metadata from a shared recipe if (this.recipeData && this.recipeData.from_recipe_metadata) { // Use title from recipe metadata if (this.recipeData.title) { recipeName.value = this.recipeData.title; this.recipeName = this.recipeData.title; } // Use tags from recipe metadata if (this.recipeData.tags && Array.isArray(this.recipeData.tags)) { this.recipeTags = [...this.recipeData.tags]; this.updateTagsDisplay(); } } else if (this.recipeData && this.recipeData.gen_params && this.recipeData.gen_params.prompt) { // Use the first 10 words from the prompt as the default recipe name const promptWords = this.recipeData.gen_params.prompt.split(' '); const truncatedPrompt = promptWords.slice(0, 10).join(' '); recipeName.value = truncatedPrompt; this.recipeName = truncatedPrompt; // Set up click handler to select all text for easy editing if (!recipeName.hasSelectAllHandler) { recipeName.addEventListener('click', function() { this.select(); }); recipeName.hasSelectAllHandler = true; } } else if (this.recipeImage && !recipeName.value) { // Fallback to image filename if no prompt is available const fileName = this.recipeImage.name.split('.')[0]; recipeName.value = fileName; this.recipeName = fileName; } // Always set up click handler for easy editing if not already set if (!recipeName.hasSelectAllHandler) { recipeName.addEventListener('click', function() { this.select(); }); recipeName.hasSelectAllHandler = true; } // Display the uploaded image in the preview const imagePreview = document.getElementById('recipeImagePreview'); if (imagePreview) { if (this.recipeImage) { // For file upload mode const reader = new FileReader(); reader.onload = (e) => { imagePreview.innerHTML = `Recipe preview`; }; reader.readAsDataURL(this.recipeImage); } else if (this.recipeData && this.recipeData.image_base64) { // For URL mode - use the base64 image data returned from the backend imagePreview.innerHTML = `Recipe preview`; } else if (this.importMode === 'url') { // Fallback for URL mode if no base64 data const urlInput = document.getElementById('imageUrlInput'); if (urlInput && urlInput.value) { imagePreview.innerHTML = `Recipe preview`; } } } // Update LoRA count information const totalLoras = this.recipeData.loras.length; const existingLoras = this.recipeData.loras.filter(lora => lora.existsLocally).length; const loraCountInfo = document.getElementById('loraCountInfo'); if (loraCountInfo) { loraCountInfo.textContent = `(${existingLoras}/${totalLoras} in library)`; } // Display LoRAs list const lorasList = document.getElementById('lorasList'); if (lorasList) { lorasList.innerHTML = this.recipeData.loras.map(lora => { const existsLocally = lora.existsLocally; const isDeleted = lora.isDeleted; const isEarlyAccess = lora.isEarlyAccess; const localPath = lora.localPath || ''; // Create status badge based on LoRA status let statusBadge; if (isDeleted) { statusBadge = `
Deleted from Civitai
`; } else { statusBadge = existsLocally ? `
In Library
${localPath}
` : `
Not in Library
`; } // Early access badge (shown additionally with other badges) let earlyAccessBadge = ''; if (isEarlyAccess) { // Format the early access end date if available let earlyAccessInfo = 'This LoRA requires early access payment to download.'; if (lora.earlyAccessEndsAt) { try { const endDate = new Date(lora.earlyAccessEndsAt); const formattedDate = endDate.toLocaleDateString(); earlyAccessInfo += ` Early access ends on ${formattedDate}.`; } catch (e) { console.warn('Failed to format early access date', e); } } earlyAccessBadge = `
Early Access
${earlyAccessInfo} Verify that you have purchased early access before downloading.
`; } // Format size if available const sizeDisplay = lora.size ? `
${this.formatFileSize(lora.size)}
` : ''; return `
LoRA preview

${lora.name}

${statusBadge} ${earlyAccessBadge}
${lora.version ? `
${lora.version}
` : ''}
${lora.baseModel ? `
${lora.baseModel}
` : ''} ${sizeDisplay}
Weight: ${lora.weight || 1.0}
`; }).join(''); } // Check for early access loras and show warning if any exist const earlyAccessLoras = this.recipeData.loras.filter(lora => lora.isEarlyAccess && !lora.existsLocally && !lora.isDeleted); if (earlyAccessLoras.length > 0) { // Show a warning about early access loras const warningMessage = `
${earlyAccessLoras.length} LoRA(s) require Early Access
These LoRAs require a payment to access. Download will fail if you haven't purchased access. You may need to log in to your Civitai account in browser settings.
`; // Show the warning message const buttonsContainer = document.querySelector('#detailsStep .modal-actions'); if (buttonsContainer) { // Remove existing warning if any const existingWarning = document.getElementById('earlyAccessWarning'); if (existingWarning) { existingWarning.remove(); } // Add new warning const warningContainer = document.createElement('div'); warningContainer.id = 'earlyAccessWarning'; warningContainer.innerHTML = warningMessage; buttonsContainer.parentNode.insertBefore(warningContainer, buttonsContainer); } } // Update Next button state based on missing LoRAs this.updateNextButtonState(); } updateNextButtonState() { const nextButton = document.querySelector('#detailsStep .primary-btn'); if (!nextButton) return; // Always clean up previous warnings first const existingWarning = document.getElementById('deletedLorasWarning'); if (existingWarning) { existingWarning.remove(); } // Count deleted LoRAs const deletedLoras = this.recipeData.loras.filter(lora => lora.isDeleted).length; // If we have deleted LoRAs, show a warning and update button text if (deletedLoras > 0) { // Create a new warning container above the buttons const buttonsContainer = document.querySelector('#detailsStep .modal-actions') || nextButton.parentNode; const warningContainer = document.createElement('div'); warningContainer.id = 'deletedLorasWarning'; warningContainer.className = 'deleted-loras-warning'; // Create warning message warningContainer.innerHTML = `
${deletedLoras} LoRA(s) have been deleted from Civitai
These LoRAs cannot be downloaded. If you continue, they will remain in the recipe but won't be included when used.
`; // Insert before the buttons container buttonsContainer.parentNode.insertBefore(warningContainer, buttonsContainer); } // If we have missing LoRAs (not deleted), show "Download Missing LoRAs" // Otherwise show "Save Recipe" const missingNotDeleted = this.recipeData.loras.filter( lora => !lora.existsLocally && !lora.isDeleted ).length; if (missingNotDeleted > 0) { nextButton.textContent = 'Download Missing LoRAs'; } else { nextButton.textContent = 'Save Recipe'; } } handleRecipeNameChange(event) { this.recipeName = event.target.value.trim(); } addTag() { const tagInput = document.getElementById('tagInput'); const tag = tagInput.value.trim(); if (!tag) return; if (!this.recipeTags.includes(tag)) { this.recipeTags.push(tag); this.updateTagsDisplay(); } tagInput.value = ''; } removeTag(tag) { this.recipeTags = this.recipeTags.filter(t => t !== tag); this.updateTagsDisplay(); } updateTagsDisplay() { const tagsContainer = document.getElementById('tagsContainer'); if (this.recipeTags.length === 0) { tagsContainer.innerHTML = '
No tags added
'; return; } tagsContainer.innerHTML = this.recipeTags.map(tag => `
${tag}
`).join(''); } proceedFromDetails() { // Validate recipe name if (!this.recipeName) { showToast('Please enter a recipe name', 'error'); return; } // Automatically mark all deleted LoRAs as excluded if (this.recipeData && this.recipeData.loras) { this.recipeData.loras.forEach(lora => { if (lora.isDeleted) { lora.exclude = true; } }); } // Update missing LoRAs list to exclude deleted LoRAs this.missingLoras = this.recipeData.loras.filter(lora => !lora.existsLocally && !lora.isDeleted); // Check for early access loras and show warning if any exist const earlyAccessLoras = this.missingLoras.filter(lora => lora.isEarlyAccess); if (earlyAccessLoras.length > 0) { // Show a warning about early access loras const warningMessage = `
${earlyAccessLoras.length} LoRA(s) require Early Access
These LoRAs require a payment to access. Download will fail if you haven't purchased access. You may need to log in to your Civitai account in browser settings.
`; // Show the warning message const buttonsContainer = document.querySelector('#detailsStep .modal-actions'); if (buttonsContainer) { // Remove existing warning if any const existingWarning = document.getElementById('earlyAccessWarning'); if (existingWarning) { existingWarning.remove(); } // Add new warning const warningContainer = document.createElement('div'); warningContainer.id = 'earlyAccessWarning'; warningContainer.innerHTML = warningMessage; buttonsContainer.parentNode.insertBefore(warningContainer, buttonsContainer); } } // If we have downloadable missing LoRAs, go to location step if (this.missingLoras.length > 0) { // Store only downloadable LoRAs for the download step this.downloadableLoRAs = this.missingLoras; this.proceedToLocation(); } else { // Otherwise, save the recipe directly this.saveRecipe(); } } async proceedToLocation() { // Show the location step with special handling this.showStep('locationStep'); // Double-check after a short delay to ensure the step is visible setTimeout(() => { const locationStep = document.getElementById('locationStep'); if (locationStep.style.display !== 'block' || window.getComputedStyle(locationStep).display !== 'block') { // Force display again locationStep.style.display = 'block'; // If still not visible, try with injected style if (window.getComputedStyle(locationStep).display !== 'block') { this.injectedStyles = document.createElement('style'); this.injectedStyles.innerHTML = ` #locationStep { display: block !important; opacity: 1 !important; visibility: visible !important; } `; document.head.appendChild(this.injectedStyles); } } }, 100); try { // Display missing LoRAs that will be downloaded const missingLorasList = document.getElementById('missingLorasList'); if (missingLorasList && this.downloadableLoRAs.length > 0) { // Calculate total size const totalSize = this.downloadableLoRAs.reduce((sum, lora) => { return sum + (lora.size ? parseInt(lora.size) : 0); }, 0); // Update total size display const totalSizeDisplay = document.getElementById('totalDownloadSize'); if (totalSizeDisplay) { totalSizeDisplay.textContent = this.formatFileSize(totalSize); } // Update header to include count of missing LoRAs const missingLorasHeader = document.querySelector('.summary-header h3'); if (missingLorasHeader) { missingLorasHeader.innerHTML = `Missing LoRAs (${this.downloadableLoRAs.length}) ${this.formatFileSize(totalSize)}`; } // Generate missing LoRAs list missingLorasList.innerHTML = this.downloadableLoRAs.map(lora => { const sizeDisplay = lora.size ? this.formatFileSize(lora.size) : 'Unknown size'; const baseModel = lora.baseModel ? `${lora.baseModel}` : ''; const isEarlyAccess = lora.isEarlyAccess; // Early access badge let earlyAccessBadge = ''; if (isEarlyAccess) { earlyAccessBadge = ` Early Access `; } return `
${lora.name}
${baseModel} ${earlyAccessBadge}
${sizeDisplay}
`; }).join(''); // Set up toggle for missing LoRAs list const toggleBtn = document.getElementById('toggleMissingLorasList'); if (toggleBtn) { toggleBtn.addEventListener('click', () => { missingLorasList.classList.toggle('collapsed'); const icon = toggleBtn.querySelector('i'); if (icon) { icon.classList.toggle('fa-chevron-down'); icon.classList.toggle('fa-chevron-up'); } }); } } // Fetch LoRA roots const rootsResponse = await fetch('/api/lora-roots'); if (!rootsResponse.ok) { throw new Error(`Failed to fetch LoRA roots: ${rootsResponse.status}`); } const rootsData = await rootsResponse.json(); const loraRoot = document.getElementById('importLoraRoot'); if (loraRoot) { loraRoot.innerHTML = rootsData.roots.map(root => `` ).join(''); // Set default lora root if available const defaultRoot = getStorageItem('settings', {}).default_loras_root; if (defaultRoot && rootsData.roots.includes(defaultRoot)) { loraRoot.value = defaultRoot; } } // Fetch folders const foldersResponse = await fetch('/api/folders'); if (!foldersResponse.ok) { throw new Error(`Failed to fetch folders: ${foldersResponse.status}`); } const foldersData = await foldersResponse.json(); const folderBrowser = document.getElementById('importFolderBrowser'); if (folderBrowser) { folderBrowser.innerHTML = foldersData.folders.map(folder => folder ? `
${folder}
` : '' ).join(''); } // Initialize folder browser after loading data this.initializeFolderBrowser(); } catch (error) { console.error('Error in API calls:', error); showToast(error.message, 'error'); } } backToUpload() { this.showStep('uploadStep'); // Reset file input to ensure it can trigger change events again const fileInput = document.getElementById('recipeImageUpload'); if (fileInput) { fileInput.value = ''; } // Reset URL input const urlInput = document.getElementById('imageUrlInput'); if (urlInput) { urlInput.value = ''; } // Clear any previous error messages const uploadError = document.getElementById('uploadError'); if (uploadError) { uploadError.textContent = ''; } const urlError = document.getElementById('urlError'); if (urlError) { urlError.textContent = ''; } } backToDetails() { this.showStep('detailsStep'); } async saveRecipe() { // 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 { this.loadingManager.showSimpleLoading(isDownloadOnly ? 'Preparing download...' : 'Saving recipe...'); // 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'); } 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; } } // Check if we need to download LoRAs let failedDownloads = 0; 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 accessFailures = 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]; // 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) { accessFailures++; this.loadingManager.setStatus( `Failed to download ${lora.name}: Access restricted` ); } 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 (accessFailures > 0) { showToast( `Downloaded ${completedDownloads} of ${this.downloadableLoRAs.length} LoRAs. ${accessFailures} failed due to access restrictions. Check your API key in settings or early access status.`, 'error' ); } else { showToast(`Downloaded ${completedDownloads} of ${this.downloadableLoRAs.length} LoRAs`, 'error'); } } } // Show success message if (isDownloadOnly) { if (failedDownloads === 0) { showToast('LoRAs downloaded successfully', 'success'); } } else { 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:', error); showToast(error.message, 'error'); } finally { this.loadingManager.hide(); } } initializeFolderBrowser() { const folderBrowser = document.getElementById('importFolderBrowser'); if (!folderBrowser) return; // Cleanup existing handler if any this.cleanupFolderBrowser(); // Create new handler this.folderClickHandler = (event) => { const folderItem = event.target.closest('.folder-item'); if (!folderItem) return; if (folderItem.classList.contains('selected')) { folderItem.classList.remove('selected'); this.selectedFolder = ''; } else { folderBrowser.querySelectorAll('.folder-item').forEach(f => f.classList.remove('selected')); folderItem.classList.add('selected'); this.selectedFolder = folderItem.dataset.folder; } // Update path display after folder selection this.updateTargetPath(); }; // Add the new handler folderBrowser.addEventListener('click', this.folderClickHandler); // Add event listeners for path updates const loraRoot = document.getElementById('importLoraRoot'); const newFolder = document.getElementById('importNewFolder'); if (loraRoot) loraRoot.addEventListener('change', this.updateTargetPath); if (newFolder) newFolder.addEventListener('input', this.updateTargetPath); // Update initial path this.updateTargetPath(); } cleanupFolderBrowser() { if (this.folderClickHandler) { const folderBrowser = document.getElementById('importFolderBrowser'); if (folderBrowser) { folderBrowser.removeEventListener('click', this.folderClickHandler); this.folderClickHandler = null; } } // Remove path update listeners const loraRoot = document.getElementById('importLoraRoot'); const newFolder = document.getElementById('importNewFolder'); if (loraRoot) loraRoot.removeEventListener('change', this.updateTargetPath); if (newFolder) newFolder.removeEventListener('input', this.updateTargetPath); } updateTargetPath() { const pathDisplay = document.getElementById('importTargetPathDisplay'); if (!pathDisplay) return; const loraRoot = document.getElementById('importLoraRoot')?.value || ''; const newFolder = document.getElementById('importNewFolder')?.value?.trim() || ''; let fullPath = loraRoot || 'Select a LoRA root directory'; if (loraRoot) { if (this.selectedFolder) { fullPath += '/' + this.selectedFolder; } if (newFolder) { fullPath += '/' + newFolder; } } pathDisplay.innerHTML = `${fullPath}`; } showStep(stepId) { // First, remove any injected styles to prevent conflicts this.removeInjectedStyles(); // Hide all steps first document.querySelectorAll('.import-step').forEach(step => { step.style.display = 'none'; }); // Show target step with a monitoring mechanism const targetStep = document.getElementById(stepId); if (targetStep) { // Use direct style setting targetStep.style.display = 'block'; // For the locationStep specifically, we need additional measures if (stepId === 'locationStep') { // Create a more persistent style to override any potential conflicts this.injectedStyles = document.createElement('style'); this.injectedStyles.innerHTML = ` #locationStep { display: block !important; opacity: 1 !important; visibility: visible !important; } `; document.head.appendChild(this.injectedStyles); // Force layout recalculation targetStep.offsetHeight; // Set up a monitor to ensure the step remains visible setTimeout(() => { if (targetStep.style.display !== 'block') { targetStep.style.display = 'block'; } // Check dimensions again after a short delay const newRect = targetStep.getBoundingClientRect(); }, 50); } // Scroll modal content to top const modalContent = document.querySelector('#importModal .modal-content'); if (modalContent) { modalContent.scrollTop = 0; } } } // Add a helper method to format file sizes formatFileSize(bytes) { if (!bytes || isNaN(bytes)) return ''; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i]; } // Add this method to ensure the modal is fully visible and initialized ensureModalVisible() { const importModal = document.getElementById('importModal'); if (!importModal) { console.error('Import modal element not found'); return false; } // Check if modal is actually visible const modalDisplay = window.getComputedStyle(importModal).display; if (modalDisplay !== 'block') { console.error('Import modal is not visible, display: ' + modalDisplay); return false; } 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'; } } }