From 71b97d597427c957b980d481a31f724af7837194 Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Mon, 5 May 2025 18:15:59 +0800 Subject: [PATCH 1/4] fix: update recipe data structure to include source_path from metadata and improve loading messages --- py/routes/recipe_routes.py | 6 +++++- py/utils/lora_metadata.py | 3 +++ static/js/managers/ImportManager.js | 21 +++++++++++++++------ 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/py/routes/recipe_routes.py b/py/routes/recipe_routes.py index d43e1972..38071d7d 100644 --- a/py/routes/recipe_routes.py +++ b/py/routes/recipe_routes.py @@ -550,7 +550,7 @@ class RecipeRoutes: with open(image_path, 'wb') as f: f.write(optimized_image) - # Create the recipe JSON + # Create the recipe data structure current_time = time.time() # Format loras data according to the recipe.json format @@ -606,6 +606,10 @@ class RecipeRoutes: if tags: recipe_data["tags"] = tags + # Add source_path if provided in metadata + if metadata.get("source_path"): + recipe_data["source_path"] = metadata.get("source_path") + # Save the recipe JSON json_filename = f"{recipe_id}.recipe.json" json_path = os.path.join(recipes_dir, json_filename) diff --git a/py/utils/lora_metadata.py b/py/utils/lora_metadata.py index f221562d..3dcecd75 100644 --- a/py/utils/lora_metadata.py +++ b/py/utils/lora_metadata.py @@ -2,6 +2,9 @@ from safetensors import safe_open from typing import Dict from .model_utils import determine_base_model import os +import logging + +logger = logging.getLogger(__name__) async def extract_lora_metadata(file_path: str) -> Dict: """Extract essential metadata from safetensors file""" diff --git a/static/js/managers/ImportManager.js b/static/js/managers/ImportManager.js index db28b4aa..ae39a141 100644 --- a/static/js/managers/ImportManager.js +++ b/static/js/managers/ImportManager.js @@ -907,17 +907,17 @@ export class ImportManager { } try { - this.loadingManager.showSimpleLoading(isDownloadOnly ? 'Preparing download...' : 'Saving recipe...'); + // Show progress indicator + this.loadingManager.showSimpleLoading(isDownloadOnly ? 'Downloading LoRAs...' : 'Saving recipe...'); - // If we're only downloading LoRAs for an existing recipe, skip the recipe save step + // Only send the complete recipe to save if not in download-only mode if (!isDownloadOnly) { - // First save the recipe - // Create form data for save request + // Create FormData object for saving recipe const formData = new FormData(); - // Handle image data - either from file upload or from URL mode + // Add image data - depends on import mode if (this.recipeImage) { - // File upload mode + // Direct upload formData.append('image', this.recipeImage); } else if (this.recipeData && this.recipeData.image_base64) { // URL mode with base64 data @@ -945,6 +945,15 @@ export class ImportManager { raw_metadata: this.recipeData.raw_metadata || {} }; + // Add source_path to metadata to track where the recipe was imported from + if (this.importMode === 'url') { + const urlInput = document.getElementById('imageUrlInput'); + console.log("urlInput.value", urlInput.value); + if (urlInput && urlInput.value) { + completeMetadata.source_path = urlInput.value; + } + } + formData.append('metadata', JSON.stringify(completeMetadata)); // Send save request From fd593bb61d2c6b1f9308782982884a1fc008861b Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Mon, 5 May 2025 20:50:32 +0800 Subject: [PATCH 2/4] feat: add source URL functionality to recipe modal, including dynamic display and editing options --- py/routes/recipe_routes.py | 4 +- static/css/components/recipe-modal.css | 163 +++++++++++++++++++++++++ static/js/components/RecipeModal.js | 93 ++++++++++++++ templates/components/recipe_modal.html | 1 + 4 files changed, 259 insertions(+), 2 deletions(-) diff --git a/py/routes/recipe_routes.py b/py/routes/recipe_routes.py index 38071d7d..dc649eba 100644 --- a/py/routes/recipe_routes.py +++ b/py/routes/recipe_routes.py @@ -1169,9 +1169,9 @@ class RecipeRoutes: data = await request.json() # Validate required fields - if 'title' not in data and 'tags' not in data: + if 'title' not in data and 'tags' not in data and 'source_path' not in data: return web.json_response({ - "error": "At least one field to update must be provided (title or tags)" + "error": "At least one field to update must be provided (title or tags or source_path)" }, status=400) # Use the recipe scanner's update method diff --git a/static/css/components/recipe-modal.css b/static/css/components/recipe-modal.css index 399ce01d..7249d151 100644 --- a/static/css/components/recipe-modal.css +++ b/static/css/components/recipe-modal.css @@ -229,8 +229,10 @@ background: var(--lora-surface); border: 1px solid var(--border-color); display: flex; + flex-direction: column; align-items: center; justify-content: center; + position: relative; } .recipe-preview-container img, @@ -246,6 +248,167 @@ object-fit: contain; } +/* Source URL container */ +.source-url-container { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: rgba(0, 0, 0, 0.5); + padding: 8px 12px; + display: flex; + justify-content: space-between; + align-items: center; + transition: transform 0.3s ease; + transform: translateY(100%); +} + +.recipe-preview-container:hover .source-url-container { + transform: translateY(0); +} + +.source-url-container.active { + transform: translateY(0); +} + +.source-url-content { + display: flex; + align-items: center; + color: #fff; + flex: 1; + overflow: hidden; + font-size: 0.85em; +} + +.source-url-icon { + margin-right: 8px; + flex-shrink: 0; +} + +.source-url-text { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + cursor: pointer; + flex: 1; +} + +.source-url-edit-btn { + background: none; + border: none; + color: #fff; + cursor: pointer; + padding: 4px; + margin-left: 8px; + border-radius: var(--border-radius-xs); + opacity: 0.7; + transition: opacity 0.2s ease; + flex-shrink: 0; +} + +.source-url-edit-btn:hover { + opacity: 1; + background: rgba(255, 255, 255, 0.1); +} + +/* Source URL editor */ +.source-url-editor { + display: none; + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: var(--bg-color); + border-top: 1px solid var(--border-color); + padding: 12px; + flex-direction: column; + gap: 10px; + z-index: 5; +} + +.source-url-editor.active { + display: flex; +} + +.source-url-input { + width: 100%; + padding: 8px 10px; + border: 1px solid var(--border-color); + border-radius: var(--border-radius-xs); + background: var(--bg-color); + color: var(--text-color); + font-size: 0.9em; +} + +.source-url-actions { + display: flex; + justify-content: flex-end; + gap: 8px; +} + +.source-url-cancel-btn, +.source-url-save-btn { + padding: 6px 12px; + border-radius: var(--border-radius-xs); + font-size: 0.85em; + cursor: pointer; + border: none; + transition: all 0.2s; +} + +.source-url-cancel-btn { + background: var(--bg-color); + color: var(--text-color); + border: 1px solid var(--border-color); +} + +.source-url-save-btn { + background: var(--lora-accent); + color: white; +} + +.source-url-cancel-btn:hover { + background: var(--lora-surface); +} + +.source-url-save-btn:hover { + background: color-mix(in oklch, var(--lora-accent), black 10%); +} + +/* Info tooltip icon for source URL */ +.source-url-info { + margin-left: 8px; + opacity: 0.7; + cursor: help; + position: relative; +} + +.source-url-info:hover { + opacity: 1; +} + +.source-url-tooltip { + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + background: var(--card-bg); + color: var(--text-color); + padding: 8px 12px; + border-radius: var(--border-radius-xs); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + white-space: nowrap; + font-size: 0.85em; + font-weight: normal; + margin-bottom: 8px; + z-index: 10; + display: none; +} + +.source-url-info:hover .source-url-tooltip { + display: block; +} + /* Generation Parameters */ .recipe-gen-params { height: 360px; diff --git a/static/js/components/RecipeModal.js b/static/js/components/RecipeModal.js index e05e43bc..399afbbb 100644 --- a/static/js/components/RecipeModal.js +++ b/static/js/components/RecipeModal.js @@ -245,6 +245,49 @@ class RecipeModal { imgElement.alt = recipe.title || 'Recipe Preview'; mediaContainer.appendChild(imgElement); } + + // Add source URL container if the recipe has a source_path + const sourceUrlContainer = document.createElement('div'); + sourceUrlContainer.className = 'source-url-container'; + const hasSourceUrl = recipe.source_path && recipe.source_path.trim().length > 0; + const sourceUrl = hasSourceUrl ? recipe.source_path : ''; + const isValidUrl = hasSourceUrl && (sourceUrl.startsWith('http://') || sourceUrl.startsWith('https://')); + + sourceUrlContainer.innerHTML = ` +
+ + ${ + hasSourceUrl ? sourceUrl : 'No source URL' + } + + + Source of the original image (e.g., Civitai post) + +
+ + `; + + // Add source URL editor + const sourceUrlEditor = document.createElement('div'); + sourceUrlEditor.className = 'source-url-editor'; + sourceUrlEditor.innerHTML = ` + +
+ + +
+ `; + + // Append both containers to the media container + mediaContainer.appendChild(sourceUrlContainer); + mediaContainer.appendChild(sourceUrlEditor); + + // Set up event listeners for source URL functionality + setTimeout(() => { + this.setupSourceUrlHandlers(); + }, 50); } // Set generation parameters @@ -1067,6 +1110,56 @@ class RecipeModal { }); }); } + + // New method to set up source URL handlers + setupSourceUrlHandlers() { + const sourceUrlContainer = document.querySelector('.source-url-container'); + const sourceUrlEditor = document.querySelector('.source-url-editor'); + const sourceUrlText = sourceUrlContainer.querySelector('.source-url-text'); + const sourceUrlEditBtn = sourceUrlContainer.querySelector('.source-url-edit-btn'); + const sourceUrlCancelBtn = sourceUrlEditor.querySelector('.source-url-cancel-btn'); + const sourceUrlSaveBtn = sourceUrlEditor.querySelector('.source-url-save-btn'); + const sourceUrlInput = sourceUrlEditor.querySelector('.source-url-input'); + + // Show editor on edit button click + sourceUrlEditBtn.addEventListener('click', () => { + sourceUrlContainer.classList.add('hide'); + sourceUrlEditor.classList.add('active'); + sourceUrlInput.focus(); + }); + + // Cancel editing + sourceUrlCancelBtn.addEventListener('click', () => { + sourceUrlEditor.classList.remove('active'); + sourceUrlContainer.classList.remove('hide'); + sourceUrlInput.value = this.currentRecipe.source_path || ''; + }); + + // Save new source URL + sourceUrlSaveBtn.addEventListener('click', () => { + const newSourceUrl = sourceUrlInput.value.trim(); + if (newSourceUrl && newSourceUrl !== this.currentRecipe.source_path) { + // Update source URL in the UI + sourceUrlText.textContent = newSourceUrl; + sourceUrlText.title = newSourceUrl.startsWith('http://') || newSourceUrl.startsWith('https://') ? 'Click to open source URL' : 'No valid URL'; + + // Update the recipe on the server + this.updateRecipeMetadata({ source_path: newSourceUrl }); + } + + // Hide editor + sourceUrlEditor.classList.remove('active'); + sourceUrlContainer.classList.remove('hide'); + }); + + // Open source URL in a new tab if it's valid + sourceUrlText.addEventListener('click', () => { + const url = sourceUrlText.textContent.trim(); + if (url.startsWith('http://') || url.startsWith('https://')) { + window.open(url, '_blank'); + } + }); + } } export { RecipeModal }; \ No newline at end of file diff --git a/templates/components/recipe_modal.html b/templates/components/recipe_modal.html index 79d0ce82..fb1b74cf 100644 --- a/templates/components/recipe_modal.html +++ b/templates/components/recipe_modal.html @@ -18,6 +18,7 @@
Recipe Preview +
From e376a45dea32116d056f9b82a6fd2ef103a66047 Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Mon, 5 May 2025 21:11:52 +0800 Subject: [PATCH 3/4] refactor: remove unused source URL tooltip from RecipeModal component --- static/css/components/recipe-modal.css | 34 -------------------------- static/js/components/RecipeModal.js | 4 --- 2 files changed, 38 deletions(-) diff --git a/static/css/components/recipe-modal.css b/static/css/components/recipe-modal.css index 7249d151..6fd1ed7e 100644 --- a/static/css/components/recipe-modal.css +++ b/static/css/components/recipe-modal.css @@ -375,40 +375,6 @@ background: color-mix(in oklch, var(--lora-accent), black 10%); } -/* Info tooltip icon for source URL */ -.source-url-info { - margin-left: 8px; - opacity: 0.7; - cursor: help; - position: relative; -} - -.source-url-info:hover { - opacity: 1; -} - -.source-url-tooltip { - position: absolute; - bottom: 100%; - left: 50%; - transform: translateX(-50%); - background: var(--card-bg); - color: var(--text-color); - padding: 8px 12px; - border-radius: var(--border-radius-xs); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); - white-space: nowrap; - font-size: 0.85em; - font-weight: normal; - margin-bottom: 8px; - z-index: 10; - display: none; -} - -.source-url-info:hover .source-url-tooltip { - display: block; -} - /* Generation Parameters */ .recipe-gen-params { height: 360px; diff --git a/static/js/components/RecipeModal.js b/static/js/components/RecipeModal.js index 399afbbb..725c8b7d 100644 --- a/static/js/components/RecipeModal.js +++ b/static/js/components/RecipeModal.js @@ -259,10 +259,6 @@ class RecipeModal { ${ hasSourceUrl ? sourceUrl : 'No source URL' } - - - Source of the original image (e.g., Civitai post) -