diff --git a/py/routes/api_routes.py b/py/routes/api_routes.py index ffb1947c..22b7ed1d 100644 --- a/py/routes/api_routes.py +++ b/py/routes/api_routes.py @@ -40,6 +40,7 @@ class ApiRoutes: app.router.add_post('/api/fetch-all-civitai', routes.fetch_all_civitai) app.router.add_get('/ws/fetch-progress', ws_manager.handle_connection) 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_post('/api/download-lora', routes.download_lora) app.router.add_post('/api/settings', routes.update_settings) @@ -520,6 +521,13 @@ class ApiRoutes: return web.json_response({ 'roots': config.loras_roots }) + + async def get_folders(self, request: web.Request) -> web.Response: + """Get all folders in the cache""" + cache = await self.scanner.get_cached_data() + return web.json_response({ + 'folders': cache.folders + }) async def get_civitai_versions(self, request: web.Request) -> web.Response: """Get available versions for a Civitai model with local availability info""" diff --git a/py/routes/recipe_routes.py b/py/routes/recipe_routes.py index 2c8cb127..e1d90f8d 100644 --- a/py/routes/recipe_routes.py +++ b/py/routes/recipe_routes.py @@ -180,7 +180,6 @@ class RecipeRoutes: # Extract metadata from the image using ExifUtils user_comment = ExifUtils.extract_user_comment(temp_path) - print(f"User comment: {user_comment}", file=sys.stderr) # If no metadata found, return a more specific error if not user_comment: @@ -191,7 +190,6 @@ class RecipeRoutes: # Parse the recipe metadata metadata = ExifUtils.parse_recipe_metadata(user_comment) - print(f"Metadata: {metadata}", file=sys.stderr) # Look for Civitai resources in the metadata civitai_resources = metadata.get('loras', []) @@ -220,7 +218,6 @@ class RecipeRoutes: # Get additional info from Civitai civitai_info = await self.civitai_client.get_model_version_info(model_version_id) - print(f"Civitai info: {civitai_info}", file=sys.stderr) # Check if this LoRA exists locally by SHA256 hash exists_locally = False diff --git a/static/css/components/import-modal.css b/static/css/components/import-modal.css index fdd1318b..5ce2a0a5 100644 --- a/static/css/components/import-modal.css +++ b/static/css/components/import-modal.css @@ -3,6 +3,341 @@ margin: var(--space-2) 0; } +/* File Input Styles */ +.file-input-wrapper { + position: relative; + margin-bottom: var(--space-1); +} + +.file-input-wrapper input[type="file"] { + position: absolute; + width: 100%; + height: 100%; + opacity: 0; + cursor: pointer; + z-index: 2; +} + +.file-input-button { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 10px 16px; + background: var(--lora-accent); + color: var(--lora-text); + border-radius: var(--border-radius-xs); + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s; +} + +.file-input-button:hover { + background: oklch(from var(--lora-accent) l c h / 0.9); +} + +.file-input-wrapper:hover .file-input-button { + background: oklch(from var(--lora-accent) l c h / 0.9); +} + +/* Recipe Details Layout */ +.recipe-details-layout { + display: grid; + grid-template-columns: 200px 1fr; + gap: var(--space-3); + margin-bottom: var(--space-3); +} + +.recipe-image-container { + width: 100%; + height: 200px; + border-radius: var(--border-radius-sm); + overflow: hidden; + background: var(--lora-surface); + border: 1px solid var(--border-color); +} + +.recipe-image { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.recipe-image img { + max-width: 100%; + max-height: 100%; + object-fit: contain; +} + +.recipe-form-container { + display: flex; + flex-direction: column; + gap: var(--space-2); +} + +/* Tags Input Styles */ +.tag-input-container { + display: flex; + gap: 8px; + margin-bottom: var(--space-1); +} + +.tag-input-container input { + flex: 1; + padding: 8px; + border: 1px solid var(--border-color); + border-radius: var(--border-radius-xs); + background: var(--bg-color); + color: var(--text-color); +} + +.tags-container { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: var(--space-1); + min-height: 32px; +} + +.recipe-tag { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + background: var(--lora-surface); + border: 1px solid var(--border-color); + border-radius: var(--border-radius-xs); + font-size: 0.9em; +} + +.recipe-tag i { + cursor: pointer; + opacity: 0.7; + transition: opacity 0.2s; +} + +.recipe-tag i:hover { + opacity: 1; + color: var(--lora-error); +} + +.empty-tags { + color: var(--text-color); + opacity: 0.6; + font-size: 0.9em; + font-style: italic; +} + +/* LoRAs List Styles */ +.loras-list { + max-height: 300px; + overflow-y: auto; + margin: var(--space-2) 0; + display: flex; + flex-direction: column; + gap: 12px; + padding: 1px; +} + +.lora-item { + display: flex; + gap: var(--space-2); + padding: var(--space-2); + border: 1px solid var(--border-color); + border-radius: var(--border-radius-sm); + background: var(--bg-color); + margin: 1px; +} + +.lora-item.exists-locally { + background: oklch(var(--lora-accent) / 0.05); + border-left: 4px solid var(--lora-accent); +} + +.lora-item.missing-locally { + border-left: 4px solid var(--lora-error); +} + +.lora-thumbnail { + width: 80px; + height: 80px; + flex-shrink: 0; + border-radius: var(--border-radius-xs); + overflow: hidden; + background: var(--bg-color); +} + +.lora-thumbnail img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.lora-content { + display: flex; + flex-direction: column; + gap: 8px; + flex: 1; + min-width: 0; +} + +.lora-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: var(--space-2); +} + +.lora-content h3 { + margin: 0; + font-size: 1.1em; + color: var(--text-color); + flex: 1; +} + +.lora-info { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; + font-size: 0.9em; +} + +.lora-info .base-model { + background: oklch(var(--lora-accent) / 0.1); + color: var(--lora-accent); + padding: 2px 8px; + border-radius: var(--border-radius-xs); +} + +.lora-version { + font-size: 0.9em; + color: var(--text-color); + opacity: 0.7; +} + +.weight-badge { + background: var(--lora-surface); + padding: 2px 8px; + border-radius: var(--border-radius-xs); + font-size: 0.85em; +} + +/* Missing LoRAs List */ +.missing-loras-list { + max-height: 200px; + overflow-y: auto; + margin: var(--space-2) 0; + display: flex; + flex-direction: column; + gap: 8px; + padding: var(--space-1); + border: 1px solid var(--border-color); + border-radius: var(--border-radius-sm); + background: var(--lora-surface); +} + +.missing-lora-item { + display: flex; + gap: var(--space-2); + padding: var(--space-1); + border-bottom: 1px solid var(--border-color); +} + +.missing-lora-item:last-child { + border-bottom: none; +} + +.missing-badge { + display: inline-flex; + align-items: center; + background: var(--lora-error); + color: white; + padding: 4px 8px; + border-radius: var(--border-radius-xs); + font-size: 0.8em; + font-weight: 500; + white-space: nowrap; + flex-shrink: 0; +} + +.missing-badge i { + margin-right: 4px; + font-size: 0.9em; +} + +.lora-count-info { + font-size: 0.85em; + opacity: 0.8; + font-weight: normal; + margin-left: 8px; +} + +/* Location Selection Styles */ +.location-selection { + margin: var(--space-2) 0; + padding: var(--space-2); + background: var(--lora-surface); + border-radius: var(--border-radius-sm); +} + +/* Reuse folder browser and path preview styles from download-modal.css */ +.folder-browser { + border: 1px solid var(--border-color); + border-radius: var(--border-radius-xs); + padding: var(--space-1); + max-height: 200px; + overflow-y: auto; +} + +.folder-item { + padding: 8px; + cursor: pointer; + border-radius: var(--border-radius-xs); + transition: background-color 0.2s; +} + +.folder-item:hover { + background: var(--lora-surface); +} + +.folder-item.selected { + background: oklch(var(--lora-accent) / 0.1); + border: 1px solid var(--lora-accent); +} + +.path-preview { + margin-bottom: var(--space-3); + padding: var(--space-2); + background: var(--bg-color); + border-radius: var(--border-radius-sm); + border: 1px dashed var(--border-color); +} + +.path-preview label { + display: block; + margin-bottom: 8px; + color: var(--text-color); + font-size: 0.9em; + opacity: 0.8; +} + +.path-display { + padding: var(--space-1); + color: var(--text-color); + font-family: monospace; + font-size: 0.9em; + line-height: 1.4; + white-space: pre-wrap; + word-break: break-all; + opacity: 0.85; + background: var(--lora-surface); + border-radius: var(--border-radius-xs); +} + +/* Input Group Styles */ .input-group { margin-bottom: var(--space-2); } @@ -29,447 +364,30 @@ margin-top: 4px; } -/* Image Upload Styles */ -.image-upload-container { +/* Modal Actions */ +.modal-actions { display: flex; - flex-direction: column; + justify-content: flex-end; gap: var(--space-2); - margin-bottom: var(--space-3); + margin-top: var(--space-3); } -.image-preview { - width: 100%; - height: 200px; - border: 2px dashed var(--border-color); - border-radius: var(--border-radius-sm); - display: flex; - align-items: center; - justify-content: center; - overflow: hidden; - background: var(--bg-color); -} - -.image-preview img { - max-width: 100%; - max-height: 100%; - object-fit: contain; -} - -.image-preview .placeholder { - color: var(--text-color); - opacity: 0.5; - font-size: 0.9em; -} - -.file-input-wrapper { - position: relative; -} - -.file-input-wrapper input[type="file"] { - opacity: 0; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - cursor: pointer; -} - -.file-input-wrapper .file-input-button { - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - padding: 8px 16px; +/* Dark theme adjustments */ +[data-theme="dark"] .lora-item { background: var(--lora-surface); - border: 1px solid var(--border-color); - border-radius: var(--border-radius-xs); - color: var(--text-color); - cursor: pointer; - transition: all 0.2s ease; } -.file-input-wrapper:hover .file-input-button { - background: var(--lora-surface-hover); -} - -/* Recipe Details Styles */ -.recipe-details-container { - display: flex; - flex-direction: column; - gap: var(--space-3); -} - -.recipe-name-container { - margin-bottom: var(--space-2); -} - -.recipe-name-container label { - display: block; - margin-bottom: 8px; - font-weight: 500; -} - -.recipe-name-container input { - width: 100%; - padding: 8px; - border: 1px solid var(--border-color); - border-radius: var(--border-radius-xs); - background: var(--bg-color); - color: var(--text-color); -} - -.tags-section { - margin-bottom: var(--space-2); -} - -.tags-section label { - display: block; - margin-bottom: 8px; - font-weight: 500; -} - -.tag-input-container { - display: flex; - gap: 8px; - margin-bottom: 8px; -} - -.tag-input-container input { - flex: 1; - padding: 8px; - border: 1px solid var(--border-color); - border-radius: var(--border-radius-xs); - background: var(--bg-color); - color: var(--text-color); -} - -.tags-container { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin-top: 8px; - min-height: 32px; -} - -.recipe-tag { - display: flex; - align-items: center; - gap: 4px; - background: var(--lora-surface); - color: var(--text-color); - padding: 4px 8px; - border-radius: var(--border-radius-xs); - font-size: 0.9em; -} - -.recipe-tag i { - cursor: pointer; - opacity: 0.7; - transition: opacity 0.2s; -} - -.recipe-tag i:hover { - opacity: 1; -} - -.empty-tags { - color: var(--text-color); - opacity: 0.5; - font-size: 0.9em; -} - -/* LoRAs List Styles */ -.loras-list { - max-height: 300px; - overflow-y: auto; - margin: var(--space-2) 0; - display: flex; - flex-direction: column; - gap: 12px; - padding: 1px; -} - -.lora-item { - display: flex; - gap: var(--space-2); - padding: var(--space-2); - border: 1px solid var(--border-color); - border-radius: var(--border-radius-sm); - background: var(--bg-color); - margin: 1px; - position: relative; -} - -.lora-item.exists-locally { - background: oklch(var(--lora-accent) / 0.05); - border-left: 4px solid var(--lora-accent); -} - -.lora-item.missing-locally { - background: oklch(var(--lora-error) / 0.05); - border-left: 4px solid var(--lora-error); -} - -.lora-thumbnail { - width: 60px; - height: 60px; - flex-shrink: 0; - border-radius: var(--border-radius-xs); - overflow: hidden; - background: var(--bg-color); -} - -.lora-thumbnail img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.lora-content { - display: flex; - flex-direction: column; - gap: 4px; - flex: 1; - min-width: 0; -} - -.lora-header { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: var(--space-2); -} - -.lora-content h3 { - margin: 0; - font-size: 1em; - color: var(--text-color); - flex: 1; -} - -.lora-info { - display: flex; - flex-wrap: wrap; - gap: 8px; - align-items: center; - font-size: 0.9em; -} - -.lora-info .base-model { - background: oklch(var(--lora-accent) / 0.1); - color: var(--lora-accent); - padding: 2px 8px; - border-radius: var(--border-radius-xs); -} - -.weight-badge { - background: var(--lora-surface); - color: var(--text-color); - padding: 2px 8px; - border-radius: var(--border-radius-xs); -} - -.lora-meta { - display: flex; - gap: 12px; - font-size: 0.85em; - color: var(--text-color); - opacity: 0.7; -} - -.lora-meta span { - display: flex; - align-items: center; - gap: 4px; -} - -/* Status Badges */ -.local-badge { - display: inline-flex; - align-items: center; - background: var(--lora-accent); - color: var(--lora-text); - padding: 4px 8px; - border-radius: var(--border-radius-xs); - font-size: 0.8em; - font-weight: 500; - white-space: nowrap; - flex-shrink: 0; - position: relative; -} - -.local-badge i { - margin-right: 4px; - font-size: 0.9em; -} - -.missing-badge { - display: inline-flex; - align-items: center; - background: var(--lora-error); - color: var(--lora-text); - padding: 4px 8px; - border-radius: var(--border-radius-xs); - font-size: 0.8em; - font-weight: 500; - white-space: nowrap; - flex-shrink: 0; -} - -.missing-badge i { - margin-right: 4px; - font-size: 0.9em; -} - -.local-path { - display: none; - position: absolute; - top: 100%; - right: 0; +[data-theme="dark"] .recipe-tag { background: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: var(--border-radius-xs); - padding: var(--space-1); - margin-top: 4px; - font-size: 0.9em; - color: var(--text-color); - white-space: normal; - word-break: break-all; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - z-index: 1; - min-width: 200px; - max-width: 300px; } -.local-badge:hover .local-path { - display: block; -} - -/* Missing LoRAs List */ -.missing-loras-list { - max-height: 150px; - overflow-y: auto; - margin: var(--space-2) 0; - display: flex; - flex-direction: column; - gap: 8px; -} - -.missing-lora-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px; - background: oklch(var(--lora-error) / 0.05); - border-left: 4px solid var(--lora-error); - border-radius: var(--border-radius-xs); -} - -.lora-name { - font-weight: 500; -} - -.lora-type { - font-size: 0.9em; - opacity: 0.7; -} - -/* Folder Browser Styles */ -.folder-browser { - border: 1px solid var(--border-color); - border-radius: var(--border-radius-xs); - padding: var(--space-1); - max-height: 200px; - overflow-y: auto; -} - -/* Modal Header Styles - Updated to match download-modal */ -.modal-header { - display: flex; - justify-content: space-between; - /* align-items: center; */ - margin-bottom: var(--space-3); - padding-bottom: var(--space-2); - border-bottom: 1px solid var(--border-color); - position: relative; -} - -.close-modal { - font-size: 1.5rem; - cursor: pointer; - opacity: 0.7; - transition: opacity 0.2s; - position: absolute; - right: 0; - top: 0; - padding: 0 5px; - line-height: 1; -} - -.close-modal:hover { - opacity: 1; -} - -/* Recipe Details Layout */ -.recipe-details-layout { - display: flex; - gap: var(--space-3); - margin-bottom: var(--space-3); -} - -.recipe-image-container { - flex: 0 0 200px; -} - -.recipe-image { - width: 100%; - height: 200px; - border-radius: var(--border-radius-sm); - overflow: hidden; - border: 1px solid var(--border-color); - background: var(--bg-color); -} - -.recipe-image img { - width: 100%; - height: 100%; - object-fit: contain; -} - -.recipe-form-container { - flex: 1; - display: flex; - flex-direction: column; - gap: var(--space-3); -} - -/* Simplify file input for step 1 */ -.file-input-wrapper { - margin: var(--space-3) auto; - max-width: 300px; -} - -/* Update LoRA item styles to include version */ -.lora-content { - display: flex; - flex-direction: column; - gap: 4px; - flex: 1; - min-width: 0; -} - -.lora-version { - font-size: 0.85em; - color: var(--text-color); - opacity: 0.7; - margin-top: 2px; -} - -.lora-count-info { - font-size: 0.85em; - font-weight: normal; - color: var(--text-color); - opacity: 0.8; - margin-left: 8px; +/* Responsive adjustments */ +@media (max-width: 768px) { + .recipe-details-layout { + grid-template-columns: 1fr; + } + + .recipe-image-container { + height: 150px; + } } diff --git a/static/js/managers/ImportManager.js b/static/js/managers/ImportManager.js index 34fc6c5c..f0cc40d3 100644 --- a/static/js/managers/ImportManager.js +++ b/static/js/managers/ImportManager.js @@ -20,10 +20,12 @@ export class ImportManager { this.loadingManager = new LoadingManager(); this.folderClickHandler = null; this.updateTargetPath = this.updateTargetPath.bind(this); + + // 添加对注入样式的引用 + this.injectedStyles = null; } showImportModal() { - console.log('Showing import modal...'); if (!this.initialized) { // Check if modal exists const modal = document.getElementById('importModal'); @@ -37,13 +39,26 @@ export class ImportManager { modalManager.showModal('importModal', null, () => { // Cleanup handler when modal closes this.cleanupFolderBrowser(); + + // 移除任何强制样式 + this.removeInjectedStyles(); }); this.resetSteps(); } + // 添加移除注入样式的方法 + removeInjectedStyles() { + if (this.injectedStyles && this.injectedStyles.parentNode) { + this.injectedStyles.parentNode.removeChild(this.injectedStyles); + this.injectedStyles = null; + } + } + resetSteps() { - document.querySelectorAll('.import-step').forEach(step => step.style.display = 'none'); - document.getElementById('uploadStep').style.display = 'block'; + // 移除可能存在的强制样式 + this.removeInjectedStyles(); + + this.showStep('uploadStep'); // Reset file input const fileInput = document.getElementById('recipeImageUpload'); @@ -147,8 +162,7 @@ export class ImportManager { } showRecipeDetailsStep() { - document.getElementById('uploadStep').style.display = 'none'; - document.getElementById('detailsStep').style.display = 'block'; + this.showStep('detailsStep'); // Set default recipe name from image filename const recipeName = document.getElementById('recipeName'); @@ -277,6 +291,8 @@ export class ImportManager { return; } + console.log('Proceeding from details, missing LoRAs:', this.missingLoras.length); + // If we have missing LoRAs, go to location step if (this.missingLoras.length > 0) { this.proceedToLocation(); @@ -287,239 +303,242 @@ export class ImportManager { } async proceedToLocation() { - document.getElementById('detailsStep').style.display = 'none'; - document.getElementById('locationStep').style.display = 'block'; + // 先移除可能已有的样式 + this.removeInjectedStyles(); + + // 添加强制CSS覆盖 + this.injectedStyles = document.createElement('style'); + this.injectedStyles.innerHTML = ` + #locationStep { + display: block !important; + opacity: 1 !important; + visibility: visible !important; + position: static !important; + z-index: 10000 !important; + width: auto !important; + height: auto !important; + overflow: visible !important; + transform: none !important; + } + `; + document.head.appendChild(this.injectedStyles); + console.log('Added override CSS to force visibility'); + + this.showStep('locationStep'); try { - this.loadingManager.showSimpleLoading('Loading download options...'); - - const response = await fetch('/api/lora-roots'); - if (!response.ok) { - throw new Error('Failed to fetch LoRA roots'); + // Fetch LoRA roots + const rootsResponse = await fetch('/api/lora-roots'); + if (!rootsResponse.ok) { + throw new Error(`Failed to fetch LoRA roots: ${rootsResponse.status}`); } - const data = await response.json(); + const rootsData = await rootsResponse.json(); const loraRoot = document.getElementById('importLoraRoot'); - - // Check if we have roots - if (!data.roots || data.roots.length === 0) { - throw new Error('No LoRA root directories configured'); + if (loraRoot) { + loraRoot.innerHTML = rootsData.roots.map(root => + `` + ).join(''); } - // Populate roots dropdown - loraRoot.innerHTML = data.roots.map(root => - `` - ).join(''); + // 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 roots - await this.initializeFolderBrowser(); - - // Display missing LoRAs - const missingLorasList = document.getElementById('missingLorasList'); - if (missingLorasList) { - missingLorasList.innerHTML = this.missingLoras.map(lora => ` -
-
${lora.name}
-
${lora.type || 'lora'}
-
- `).join(''); - } - - // Update target path display - this.updateTargetPath(); - + // Initialize folder browser after loading data + this.initializeFolderBrowser(); } catch (error) { - console.error('Error in proceedToLocation:', error); + console.error('Error in API calls:', error); showToast(error.message, 'error'); - // Go back to details step on error - this.backToDetails(); - } finally { - this.loadingManager.hide(); } } backToUpload() { - document.getElementById('detailsStep').style.display = 'none'; - document.getElementById('uploadStep').style.display = 'block'; + this.showStep('uploadStep'); } backToDetails() { - document.getElementById('locationStep').style.display = 'none'; - document.getElementById('detailsStep').style.display = 'block'; + this.showStep('detailsStep'); } async saveRecipe() { + if (!this.recipeName) { + showToast('Please enter a recipe name', 'error'); + return; + } + try { - // If we're in the location step, we need to download missing LoRAs first - if (document.getElementById('locationStep').style.display !== 'none') { - const loraRoot = document.getElementById('importLoraRoot').value; - const newFolder = document.getElementById('importNewFolder').value.trim(); - - if (!loraRoot) { - showToast('Please select a LoRA root directory', 'error'); - return; - } - - // Construct relative path - let targetFolder = ''; - if (this.selectedFolder) { - targetFolder = this.selectedFolder; - } - if (newFolder) { - targetFolder = targetFolder ? - `${targetFolder}/${newFolder}` : newFolder; - } - - // Show loading with progress bar for download - this.loadingManager.show('Downloading missing LoRAs...', 0); - - // Setup WebSocket for progress updates - const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://'; - const ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/fetch-progress`); - ws.onmessage = (event) => { - const data = JSON.parse(event.data); - if (data.status === 'progress') { - this.loadingManager.setProgress(data.progress); - this.loadingManager.setStatus(`Downloading: ${data.progress}%`); - } - }; - - // Download missing LoRAs - const downloadResponse = await fetch('/api/recipes/download-missing-loras', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - loras: this.missingLoras, - lora_root: loraRoot, - relative_path: targetFolder - }) - }); - - if (!downloadResponse.ok) { - throw new Error(await downloadResponse.text()); - } - - // Update missing LoRAs with downloaded paths - const downloadResult = await downloadResponse.json(); - this.recipeData.loras = this.recipeData.loras.map(lora => { - const downloaded = downloadResult.downloaded.find(d => d.id === lora.id); - if (downloaded) { - return { - ...lora, - existsLocally: true, - localPath: downloaded.localPath - }; - } - return lora; - }); - } - - // Now save the recipe + // First save the recipe this.loadingManager.showSimpleLoading('Saving recipe...'); - // Create form data for recipe save + // Create form data for save request const formData = new FormData(); formData.append('image', this.recipeImage); formData.append('name', this.recipeName); formData.append('tags', JSON.stringify(this.recipeTags)); - formData.append('recipe_data', JSON.stringify(this.recipeData)); + formData.append('metadata', JSON.stringify(this.recipeData)); - // Save recipe - const saveResponse = await fetch('/api/recipes/save', { + // Send save request + const response = await fetch('/api/recipes/save', { method: 'POST', body: formData }); - if (!saveResponse.ok) { - throw new Error(await saveResponse.text()); + const result = await response.json(); + if (!result.success) { + throw new Error(result.error || 'Failed to save recipe'); } - showToast('Recipe saved successfully', 'success'); + // Show success message for recipe save + showToast(`Recipe "${this.recipeName}" saved successfully`, 'success'); + + // Check if we need to download LoRAs + if (this.missingLoras.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`); + + // Download missing LoRAs sequentially + this.loadingManager.show('Downloading LoRAs...', 0); + + let completedDownloads = 0; + for (let i = 0; i < this.missingLoras.length; i++) { + const lora = this.missingLoras[i]; + + // Update overall progress + this.loadingManager.setStatus(`Downloading LoRA ${i+1}/${this.missingLoras.length}: ${lora.name}`); + + // Set up progress tracking for current download + ws.onmessage = (event) => { + const data = JSON.parse(event.data); + if (data.status === 'progress') { + // Calculate overall progress: completed files + current file progress + const overallProgress = Math.floor( + (completedDownloads + data.progress/100) / this.missingLoras.length * 100 + ); + this.loadingManager.setProgress(overallProgress); + } + }; + + 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}`); + // Continue with next download + } else { + completedDownloads++; + } + } catch (downloadError) { + console.error(`Error downloading LoRA ${lora.name}:`, downloadError); + // Continue with next download + } + } + + // Close WebSocket + ws.close(); + + // Show final completion message + if (completedDownloads === this.missingLoras.length) { + showToast(`All ${completedDownloads} LoRAs downloaded successfully`, 'success'); + } else { + showToast(`Downloaded ${completedDownloads} of ${this.missingLoras.length} LoRAs`, 'warning'); + } + } + + // Close modal and reload recipes modalManager.closeModal('importModal'); - // Reload recipes - window.location.reload(); + // Refresh the recipe list if needed + if (typeof refreshRecipes === 'function') { + refreshRecipes(); + } else { + // Fallback to reloading the page + resetAndReload(); + } } catch (error) { + console.error('Error saving recipe:', error); showToast(error.message, 'error'); } finally { this.loadingManager.hide(); } } - // Add new method to handle folder selection - async initializeFolderBrowser() { + initializeFolderBrowser() { const folderBrowser = document.getElementById('importFolderBrowser'); if (!folderBrowser) return; // Cleanup existing handler if any this.cleanupFolderBrowser(); - try { - // Get the selected root - const loraRoot = document.getElementById('importLoraRoot').value; - if (!loraRoot) { - folderBrowser.innerHTML = '
Please select a LoRA root directory
'; - return; - } - - // Fetch folders for the selected root - const response = await fetch(`/api/folders?root=${encodeURIComponent(loraRoot)}`); - if (!response.ok) { - throw new Error('Failed to fetch folders'); - } - - const data = await response.json(); - - // Display folders - if (data.folders && data.folders.length > 0) { - folderBrowser.innerHTML = data.folders.map(folder => ` -
- ${folder} -
- `).join(''); + // 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.innerHTML = '
No folders found
'; + folderBrowser.querySelectorAll('.folder-item').forEach(f => + f.classList.remove('selected')); + folderItem.classList.add('selected'); + this.selectedFolder = folderItem.dataset.folder; } - // Create new handler - this.folderClickHandler = (event) => { - const folderItem = event.target.closest('.folder-item'); - if (!folderItem) return; + // Update path display after folder selection + this.updateTargetPath(); + }; - 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); - - } catch (error) { - console.error('Error initializing folder browser:', error); - folderBrowser.innerHTML = `
Error: ${error.message}
`; - } + // 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'); - loraRoot.addEventListener('change', async () => { - await this.initializeFolderBrowser(); - this.updateTargetPath(); - }); - - newFolder.addEventListener('input', this.updateTargetPath); + if (loraRoot) loraRoot.addEventListener('change', this.updateTargetPath); + if (newFolder) newFolder.addEventListener('input', this.updateTargetPath); + console.log('Initializing folder browser...'); // Update initial path this.updateTargetPath(); } @@ -541,15 +560,14 @@ export class ImportManager { if (newFolder) newFolder.removeEventListener('input', this.updateTargetPath); } - // Add new method to update target path updateTargetPath() { const pathDisplay = document.getElementById('importTargetPathDisplay'); if (!pathDisplay) return; const loraRoot = document.getElementById('importLoraRoot')?.value || ''; - const newFolder = document.getElementById('importNewFolder')?.value.trim() || ''; + const newFolder = document.getElementById('importNewFolder')?.value?.trim() || ''; - let fullPath = loraRoot || 'Select a LoRA root directory'; + let fullPath = loraRoot || 'Select a LoRA root directory'; if (loraRoot) { if (this.selectedFolder) { @@ -559,7 +577,41 @@ export class ImportManager { fullPath += '/' + newFolder; } } - + pathDisplay.innerHTML = `${fullPath}`; } + + showStep(stepId) { + // 隐藏所有步骤 + document.querySelectorAll('.import-step').forEach(step => { + step.style.display = 'none'; + }); + + // 显示目标步骤 + const targetStep = document.getElementById(stepId); + if (targetStep) { + // 强制显示目标步骤 - 使用 !important 覆盖任何其他CSS规则 + targetStep.setAttribute('style', 'display: block !important'); + + // 调试信息 + console.log(`Showing step: ${stepId}`); + const rect = targetStep.getBoundingClientRect(); + console.log('Step dimensions:', { + width: rect.width, + height: rect.height, + top: rect.top, + left: rect.left, + visible: rect.width > 0 && rect.height > 0 + }); + + // 强制重新计算布局 + targetStep.offsetHeight; + + // 滚动模态内容到顶部 + const modalContent = document.querySelector('#importModal .modal-content'); + if (modalContent) { + modalContent.scrollTop = 0; + } + } + } } \ No newline at end of file diff --git a/templates/components/import_modal.html b/templates/components/import_modal.html index 90c4e17e..1151f7d3 100644 --- a/templates/components/import_modal.html +++ b/templates/components/import_modal.html @@ -1,106 +1,102 @@