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; // Change default mode to url/path this.importMode = 'url'; // Default mode changed to: 'url' or 'upload' } 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 = '
'; } // Reset state variables this.recipeImage = null; this.recipeData = null; this.recipeName = ''; this.recipeTags = []; this.missingLoras = []; this.downloadableLoRAs = []; // Reset import mode to url/path instead of upload this.importMode = 'url'; this.toggleImportMode('url'); // 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 input = urlInput.value.trim(); // Validate input if (!input) { errorElement.textContent = 'Please enter a URL or file path'; return; } // Reset error errorElement.textContent = ''; // Show loading indicator this.loadingManager.showSimpleLoading('Processing input...'); try { // Check if it's a URL or a local file path if (input.startsWith('http://') || input.startsWith('https://')) { // Handle as URL await this.analyzeImageFromUrl(input); } else { // Handle as local file path await this.analyzeImageFromLocalPath(input); } } catch (error) { errorElement.textContent = error.message || 'Failed to process input'; } 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; } } // Add new method to handle local file paths async analyzeImageFromLocalPath(path) { try { // Call the API with local path data const response = await fetch('/api/recipes/analyze-local-image', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: path }) }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || 'Failed to load image from local path'); } // 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 local path:', 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 = `