From b65350b7cbcd101cd7411c9e714fe64c7972651e Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Sat, 29 Mar 2025 18:35:49 +0800 Subject: [PATCH] Add update functionality for recipe metadata in RecipeRoutes and RecipeModal - Introduced a new API endpoint to update recipe metadata, allowing users to modify recipe titles and tags. - Enhanced RecipeModal to support inline editing of recipe titles and tags, improving user interaction. - Updated RecipeCard to reflect changes in recipe metadata, ensuring consistency across the application. - Improved error handling for metadata updates to provide clearer feedback to users. --- py/routes/recipe_routes.py | 30 ++ py/services/recipe_cache.py | 24 +- py/services/recipe_scanner.py | 48 +++ static/css/components/recipe-modal.css | 145 +++++++- static/js/components/RecipeCard.js | 2 +- static/js/components/RecipeModal.js | 483 +++++++++++++++++++++---- 6 files changed, 653 insertions(+), 79 deletions(-) diff --git a/py/routes/recipe_routes.py b/py/routes/recipe_routes.py index 24fa47ea..b58b58ed 100644 --- a/py/routes/recipe_routes.py +++ b/py/routes/recipe_routes.py @@ -50,6 +50,9 @@ class RecipeRoutes: # Add new endpoint for getting recipe syntax app.router.add_get('/api/recipe/{recipe_id}/syntax', routes.get_recipe_syntax) + # Add new endpoint for updating recipe metadata (name and tags) + app.router.add_put('/api/recipe/{recipe_id}/update', routes.update_recipe) + # Start cache initialization app.on_startup.append(routes._init_cache) @@ -984,3 +987,30 @@ class RecipeRoutes: except Exception as e: logger.error(f"Error generating recipe syntax: {e}", exc_info=True) return web.json_response({"error": str(e)}, status=500) + + async def update_recipe(self, request: web.Request) -> web.Response: + """Update recipe metadata (name and tags)""" + try: + recipe_id = request.match_info['recipe_id'] + data = await request.json() + + # Validate required fields + if 'title' not in data and 'tags' not in data: + return web.json_response({ + "error": "At least one field to update must be provided (title or tags)" + }, status=400) + + # Use the recipe scanner's update method + success = await self.recipe_scanner.update_recipe_metadata(recipe_id, data) + + if not success: + return web.json_response({"error": "Recipe not found or update failed"}, status=404) + + return web.json_response({ + "success": True, + "recipe_id": recipe_id, + "updates": data + }) + except Exception as e: + logger.error(f"Error updating recipe: {e}", exc_info=True) + return web.json_response({"error": str(e)}, status=500) diff --git a/py/services/recipe_cache.py b/py/services/recipe_cache.py index b9cd2219..0b7d7da9 100644 --- a/py/services/recipe_cache.py +++ b/py/services/recipe_cache.py @@ -37,18 +37,18 @@ class RecipeCache: Returns: bool: True if the update was successful, False if the recipe wasn't found """ - async with self._lock: - # Update in raw_data - for item in self.raw_data: - if item.get('id') == recipe_id: - item.update(metadata) - break - else: - return False # Recipe not found - - # Resort to reflect changes - await self.resort() - return True + + # Update in raw_data + for item in self.raw_data: + if item.get('id') == recipe_id: + item.update(metadata) + break + else: + return False # Recipe not found + + # Resort to reflect changes + await self.resort() + return True async def add_recipe(self, recipe_data: Dict) -> None: """Add a new recipe to the cache diff --git a/py/services/recipe_scanner.py b/py/services/recipe_scanner.py index aeb27516..3db5f884 100644 --- a/py/services/recipe_scanner.py +++ b/py/services/recipe_scanner.py @@ -431,6 +431,54 @@ class RecipeScanner: return result + async def update_recipe_metadata(self, recipe_id: str, metadata: dict) -> bool: + """Update recipe metadata (like title and tags) in both file system and cache + + Args: + recipe_id: The ID of the recipe to update + metadata: Dictionary containing metadata fields to update (title, tags, etc.) + + Returns: + bool: True if successful, False otherwise + """ + import os + import json + + # First, find the recipe JSON file path + recipe_json_path = os.path.join(self.recipes_dir, f"{recipe_id}.recipe.json") + + if not os.path.exists(recipe_json_path): + return False + + try: + # Load existing recipe data + with open(recipe_json_path, 'r', encoding='utf-8') as f: + recipe_data = json.load(f) + + # Update fields + for key, value in metadata.items(): + recipe_data[key] = value + + # Save updated recipe + with open(recipe_json_path, 'w', encoding='utf-8') as f: + json.dump(recipe_data, f, indent=4, ensure_ascii=False) + + # Update the cache if it exists + if self._cache is not None: + await self._cache.update_recipe_metadata(recipe_id, metadata) + + # If the recipe has an image, update its EXIF metadata + from ..utils.exif_utils import ExifUtils + image_path = recipe_data.get('file_path') + if image_path and os.path.exists(image_path): + ExifUtils.append_recipe_metadata(image_path, recipe_data) + + return True + except Exception as e: + import logging + logging.getLogger(__name__).error(f"Error updating recipe metadata: {e}", exc_info=True) + return False + async def update_lora_filename_by_hash(self, hash_value: str, new_file_name: str) -> Tuple[int, int]: """Update file_name in all recipes that contain a LoRA with the specified hash. diff --git a/static/css/components/recipe-modal.css b/static/css/components/recipe-modal.css index 8f792cb5..27f16b4f 100644 --- a/static/css/components/recipe-modal.css +++ b/static/css/components/recipe-modal.css @@ -18,6 +18,110 @@ display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; + width: calc(100% - 20px); +} + +/* Editable content styles */ +.editable-content { + position: relative; + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; +} + +.editable-content.hide { + display: none; +} + +.editable-content .content-text { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; +} + +.edit-icon { + background: none; + border: none; + color: var(--text-color); + opacity: 0; + cursor: pointer; + padding: 4px 8px; + margin-left: 8px; + border-radius: var(--border-radius-xs); + transition: all 0.2s; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.editable-content:hover .edit-icon { + opacity: 0.6; +} + +.edit-icon:hover { + opacity: 1 !important; + background: var(--lora-surface); +} + +/* Content editor styles */ +.content-editor { + display: none; + width: 100%; + padding: 4px 0; +} + +.content-editor.active { + display: flex; + align-items: center; + gap: 8px; +} + +.content-editor input { + flex: 1; + background: var(--bg-color); + border: 1px solid var(--lora-border); + border-radius: var(--border-radius-xs); + padding: 6px 8px; + font-size: 1em; + color: var(--text-color); + min-width: 0; +} + +.content-editor.tags-editor input { + font-size: 0.9em; +} + +/* 删除不再需要的按钮样式 */ +.editor-actions { + display: none; +} + +/* Special styling for tags content */ +.tags-content { + display: flex; + align-items: center; + flex-wrap: nowrap; + gap: 8px; +} + +.tags-display { + display: flex; + flex-wrap: nowrap; + gap: 6px; + align-items: center; + flex: 1; + min-width: 0; + overflow: hidden; +} + +.no-tags { + font-size: 0.85em; + color: var(--text-color); + opacity: 0.6; + font-style: italic; } /* Recipe Tags styles */ @@ -340,6 +444,12 @@ border-left: 4px solid var(--lora-error); } +.recipe-lora-item.is-deleted { + background: rgba(127, 127, 127, 0.05); + border-left: 4px solid #777; + opacity: 0.8; +} + .recipe-lora-thumbnail { width: 46px; height: 46px; @@ -474,6 +584,38 @@ font-size: 0.9em; } +/* Deleted badge */ +.deleted-badge { + display: inline-flex; + align-items: center; + background: #777; + color: white; + padding: 3px 6px; + border-radius: var(--border-radius-xs); + font-size: 0.75em; + font-weight: 500; + white-space: nowrap; + flex-shrink: 0; +} + +.deleted-badge i { + margin-right: 4px; + font-size: 0.9em; +} + +/* Recipe status partial state */ +.recipe-status.partial { + background: rgba(127, 127, 127, 0.1); + color: #777; +} + +/* 标题输入框特定的样式 */ +.title-input { + font-size: 1.2em !important; /* 调整为更合适的大小 */ + line-height: 1.2; + font-weight: 500; +} + /* Responsive adjustments */ @media (max-width: 768px) { .recipe-top-section { @@ -502,7 +644,8 @@ /* Update the local-badge and missing-badge to be positioned within the badge-container */ .badge-container .local-badge, -.badge-container .missing-badge { +.badge-container .missing-badge, +.badge-container .deleted-badge { position: static; /* Override absolute positioning */ transform: none; /* Remove the transform */ } diff --git a/static/js/components/RecipeCard.js b/static/js/components/RecipeCard.js index 8e3b1b7e..d4285b21 100644 --- a/static/js/components/RecipeCard.js +++ b/static/js/components/RecipeCard.js @@ -25,7 +25,7 @@ class RecipeCard { const lorasCount = loras.length; // Check if all LoRAs are available in the library - const missingLorasCount = loras.filter(lora => !lora.inLibrary).length; + const missingLorasCount = loras.filter(lora => !lora.inLibrary && !lora.isDeleted).length; const allLorasAvailable = missingLorasCount === 0 && lorasCount > 0; // Ensure file_url exists, fallback to file_path if needed diff --git a/static/js/components/RecipeModal.js b/static/js/components/RecipeModal.js index c25313ad..b4d4b7cc 100644 --- a/static/js/components/RecipeModal.js +++ b/static/js/components/RecipeModal.js @@ -12,6 +12,25 @@ class RecipeModal { document.addEventListener('DOMContentLoaded', () => { this.setupTooltipPositioning(); }); + + // Set up document click handler to close edit fields + document.addEventListener('click', (event) => { + // Handle title edit + const titleEditor = document.getElementById('recipeTitleEditor'); + if (titleEditor && titleEditor.classList.contains('active') && + !titleEditor.contains(event.target) && + !event.target.closest('.edit-icon')) { + this.saveTitleEdit(); + } + + // Handle tags edit + const tagsEditor = document.getElementById('recipeTagsEditor'); + if (tagsEditor && tagsEditor.classList.contains('active') && + !tagsEditor.contains(event.target) && + !event.target.closest('.edit-icon')) { + this.saveTagsEdit(); + } + }); } // Add tooltip positioning handler to ensure correct positioning of fixed tooltips @@ -35,10 +54,37 @@ class RecipeModal { } showRecipeDetails(recipe) { - // Set modal title + // Store the full recipe for editing + this.currentRecipe = JSON.parse(JSON.stringify(recipe)); // 深拷贝以避免对原始对象的修改 + + // Set modal title with edit icon const modalTitle = document.getElementById('recipeModalTitle'); if (modalTitle) { - modalTitle.textContent = recipe.title || 'Recipe Details'; + modalTitle.innerHTML = ` +
+ ${recipe.title || 'Recipe Details'} + +
+
+ +
+ `; + + // Add event listener for title editing + const editIcon = modalTitle.querySelector('.edit-icon'); + editIcon.addEventListener('click', () => this.showTitleEditor()); + + // Add key event listener for Enter key + const titleInput = modalTitle.querySelector('.title-input'); + titleInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + this.saveTitleEdit(); + } else if (e.key === 'Escape') { + e.preventDefault(); + this.cancelTitleEdit(); + } + }); } // Store the recipe ID for copy syntax API call @@ -48,59 +94,94 @@ class RecipeModal { const tagsCompactElement = document.getElementById('recipeTagsCompact'); const tagsTooltipContent = document.getElementById('recipeTagsTooltipContent'); - if (tagsCompactElement && tagsTooltipContent && recipe.tags && recipe.tags.length > 0) { - // Clear previous tags - tagsCompactElement.innerHTML = ''; - tagsTooltipContent.innerHTML = ''; + if (tagsCompactElement) { + // Add tags container with edit functionality + tagsCompactElement.innerHTML = ` +
+
+ +
+
+ +
+ `; - // Limit displayed tags to 5, show a "+X more" button if needed - const maxVisibleTags = 5; - const visibleTags = recipe.tags.slice(0, maxVisibleTags); - const remainingTags = recipe.tags.length > maxVisibleTags ? recipe.tags.slice(maxVisibleTags) : []; + const tagsDisplay = tagsCompactElement.querySelector('.tags-display'); - // Add visible tags - visibleTags.forEach(tag => { - const tagElement = document.createElement('div'); - tagElement.className = 'recipe-tag-compact'; - tagElement.textContent = tag; - tagsCompactElement.appendChild(tagElement); - }); - - // Add "more" button if needed - if (remainingTags.length > 0) { - const moreButton = document.createElement('div'); - moreButton.className = 'recipe-tag-more'; - moreButton.textContent = `+${remainingTags.length} more`; - tagsCompactElement.appendChild(moreButton); + if (recipe.tags && recipe.tags.length > 0) { + // Limit displayed tags to 5, show a "+X more" button if needed + const maxVisibleTags = 5; + const visibleTags = recipe.tags.slice(0, maxVisibleTags); + const remainingTags = recipe.tags.length > maxVisibleTags ? recipe.tags.slice(maxVisibleTags) : []; - // Add tooltip functionality - moreButton.addEventListener('mouseenter', () => { - document.getElementById('recipeTagsTooltip').classList.add('visible'); + // Add visible tags + visibleTags.forEach(tag => { + const tagElement = document.createElement('div'); + tagElement.className = 'recipe-tag-compact'; + tagElement.textContent = tag; + tagsDisplay.appendChild(tagElement); }); - moreButton.addEventListener('mouseleave', () => { - setTimeout(() => { - if (!document.getElementById('recipeTagsTooltip').matches(':hover')) { - document.getElementById('recipeTagsTooltip').classList.remove('visible'); - } - }, 300); - }); - - document.getElementById('recipeTagsTooltip').addEventListener('mouseleave', () => { - document.getElementById('recipeTagsTooltip').classList.remove('visible'); - }); - - // Add all tags to tooltip - recipe.tags.forEach(tag => { - const tooltipTag = document.createElement('div'); - tooltipTag.className = 'tooltip-tag'; - tooltipTag.textContent = tag; - tagsTooltipContent.appendChild(tooltipTag); - }); + // Add "more" button if needed + if (remainingTags.length > 0) { + const moreButton = document.createElement('div'); + moreButton.className = 'recipe-tag-more'; + moreButton.textContent = `+${remainingTags.length} more`; + tagsDisplay.appendChild(moreButton); + + // Add tooltip functionality + moreButton.addEventListener('mouseenter', () => { + document.getElementById('recipeTagsTooltip').classList.add('visible'); + }); + + moreButton.addEventListener('mouseleave', () => { + setTimeout(() => { + if (!document.getElementById('recipeTagsTooltip').matches(':hover')) { + document.getElementById('recipeTagsTooltip').classList.remove('visible'); + } + }, 300); + }); + + document.getElementById('recipeTagsTooltip').addEventListener('mouseleave', () => { + document.getElementById('recipeTagsTooltip').classList.remove('visible'); + }); + + // Add all tags to tooltip + if (tagsTooltipContent) { + tagsTooltipContent.innerHTML = ''; + recipe.tags.forEach(tag => { + const tooltipTag = document.createElement('div'); + tooltipTag.className = 'tooltip-tag'; + tooltipTag.textContent = tag; + tagsTooltipContent.appendChild(tooltipTag); + }); + } + } + } else { + tagsDisplay.innerHTML = '
No tags
'; } - } else if (tagsCompactElement) { - // No tags to display - tagsCompactElement.innerHTML = ''; + + // Add event listeners for tags editing + const editTagsIcon = tagsCompactElement.querySelector('.edit-icon'); + const tagsInput = tagsCompactElement.querySelector('.tags-input'); + + // Set current tags in the input + if (recipe.tags && recipe.tags.length > 0) { + tagsInput.value = recipe.tags.join(', '); + } + + editTagsIcon.addEventListener('click', () => this.showTagsEditor()); + + // Add key event listener for Enter key + tagsInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + this.saveTagsEdit(); + } else if (e.key === 'Escape') { + e.preventDefault(); + this.cancelTagsEdit(); + } + }); } // Set recipe image @@ -195,30 +276,38 @@ class RecipeModal { const lorasListElement = document.getElementById('recipeLorasList'); const lorasCountElement = document.getElementById('recipeLorasCount'); - // 检查所有 LoRAs 是否都在库中 + // Check all LoRAs status let allLorasAvailable = true; let missingLorasCount = 0; + let deletedLorasCount = 0; if (recipe.loras && recipe.loras.length > 0) { recipe.loras.forEach(lora => { - if (!lora.inLibrary) { + if (lora.isDeleted) { + deletedLorasCount++; + } else if (!lora.inLibrary) { allLorasAvailable = false; missingLorasCount++; } }); } - // 设置 LoRAs 计数和状态 + // Set LoRAs count and status if (lorasCountElement && recipe.loras) { const totalCount = recipe.loras.length; - // 创建状态指示器 + // Create status indicator based on LoRA states let statusHTML = ''; if (totalCount > 0) { - if (allLorasAvailable) { + if (allLorasAvailable && deletedLorasCount === 0) { + // All LoRAs are available statusHTML = `
Ready to use
`; - } else { + } else if (missingLorasCount > 0) { + // Some LoRAs are missing (prioritize showing missing over deleted) statusHTML = `
${missingLorasCount} missing
`; + } else if (deletedLorasCount > 0 && missingLorasCount === 0) { + // Some LoRAs are deleted but none are missing + statusHTML = `
${deletedLorasCount} deleted
`; } } @@ -228,17 +317,28 @@ class RecipeModal { if (lorasListElement && recipe.loras && recipe.loras.length > 0) { lorasListElement.innerHTML = recipe.loras.map(lora => { const existsLocally = lora.inLibrary; + const isDeleted = lora.isDeleted; const localPath = lora.localPath || ''; - // Create local status badge with a more stable structure - const localStatus = existsLocally ? - `
- In Library -
${localPath}
-
` : - `
- Not in Library -
`; + // Create status badge based on LoRA state + let localStatus; + if (existsLocally) { + localStatus = ` +
+ In Library +
${localPath}
+
`; + } else if (isDeleted) { + localStatus = ` +
+ Deleted +
`; + } else { + localStatus = ` +
+ Not in Library +
`; + } // Check if preview is a video const isPreviewVideo = lora.preview_url && lora.preview_url.toLowerCase().endsWith('.mp4'); @@ -248,8 +348,18 @@ class RecipeModal { ` : `LoRA preview`; + // Determine CSS class based on LoRA state + let loraItemClass = 'recipe-lora-item'; + if (existsLocally) { + loraItemClass += ' exists-locally'; + } else if (isDeleted) { + loraItemClass += ' is-deleted'; + } else { + loraItemClass += ' missing-locally'; + } + return ` -
+
${previewMedia}
@@ -280,6 +390,249 @@ class RecipeModal { modalManager.showModal('recipeModal'); } + // Title editing methods + showTitleEditor() { + const titleContainer = document.getElementById('recipeModalTitle'); + if (titleContainer) { + titleContainer.querySelector('.editable-content').classList.add('hide'); + const editor = titleContainer.querySelector('#recipeTitleEditor'); + editor.classList.add('active'); + const input = editor.querySelector('input'); + input.focus(); + input.select(); + } + } + + saveTitleEdit() { + const titleContainer = document.getElementById('recipeModalTitle'); + if (titleContainer) { + const editor = titleContainer.querySelector('#recipeTitleEditor'); + const input = editor.querySelector('input'); + const newTitle = input.value.trim(); + + // Check if title changed + if (newTitle && newTitle !== this.currentRecipe.title) { + // Update title in the UI + titleContainer.querySelector('.content-text').textContent = newTitle; + + // Update the recipe on the server + this.updateRecipeMetadata({ title: newTitle }); + } + + // Hide editor + editor.classList.remove('active'); + titleContainer.querySelector('.editable-content').classList.remove('hide'); + } + } + + cancelTitleEdit() { + const titleContainer = document.getElementById('recipeModalTitle'); + if (titleContainer) { + // Reset input value + const editor = titleContainer.querySelector('#recipeTitleEditor'); + const input = editor.querySelector('input'); + input.value = this.currentRecipe.title || ''; + + // Hide editor + editor.classList.remove('active'); + titleContainer.querySelector('.editable-content').classList.remove('hide'); + } + } + + // Tags editing methods + showTagsEditor() { + const tagsContainer = document.getElementById('recipeTagsCompact'); + if (tagsContainer) { + tagsContainer.querySelector('.editable-content').classList.add('hide'); + const editor = tagsContainer.querySelector('#recipeTagsEditor'); + editor.classList.add('active'); + const input = editor.querySelector('input'); + input.focus(); + } + } + + saveTagsEdit() { + const tagsContainer = document.getElementById('recipeTagsCompact'); + if (tagsContainer) { + const editor = tagsContainer.querySelector('#recipeTagsEditor'); + const input = editor.querySelector('input'); + const tagsText = input.value.trim(); + + // Parse tags + let newTags = []; + if (tagsText) { + newTags = tagsText.split(',') + .map(tag => tag.trim()) + .filter(tag => tag.length > 0); + } + + // Check if tags changed + const oldTags = this.currentRecipe.tags || []; + const tagsChanged = + newTags.length !== oldTags.length || + newTags.some((tag, index) => tag !== oldTags[index]); + + if (tagsChanged) { + // Update the recipe on the server + this.updateRecipeMetadata({ tags: newTags }); + + // Update tags in the UI + const tagsDisplay = tagsContainer.querySelector('.tags-display'); + tagsDisplay.innerHTML = ''; + + if (newTags.length > 0) { + // Limit displayed tags to 5, show a "+X more" button if needed + const maxVisibleTags = 5; + const visibleTags = newTags.slice(0, maxVisibleTags); + const remainingTags = newTags.length > maxVisibleTags ? newTags.slice(maxVisibleTags) : []; + + // Add visible tags + visibleTags.forEach(tag => { + const tagElement = document.createElement('div'); + tagElement.className = 'recipe-tag-compact'; + tagElement.textContent = tag; + tagsDisplay.appendChild(tagElement); + }); + + // Add "more" button if needed + if (remainingTags.length > 0) { + const moreButton = document.createElement('div'); + moreButton.className = 'recipe-tag-more'; + moreButton.textContent = `+${remainingTags.length} more`; + tagsDisplay.appendChild(moreButton); + + // Update tooltip content + const tooltipContent = document.getElementById('recipeTagsTooltipContent'); + if (tooltipContent) { + tooltipContent.innerHTML = ''; + newTags.forEach(tag => { + const tooltipTag = document.createElement('div'); + tooltipTag.className = 'tooltip-tag'; + tooltipTag.textContent = tag; + tooltipContent.appendChild(tooltipTag); + }); + } + + // Re-add tooltip functionality + moreButton.addEventListener('mouseenter', () => { + document.getElementById('recipeTagsTooltip').classList.add('visible'); + }); + + moreButton.addEventListener('mouseleave', () => { + setTimeout(() => { + if (!document.getElementById('recipeTagsTooltip').matches(':hover')) { + document.getElementById('recipeTagsTooltip').classList.remove('visible'); + } + }, 300); + }); + } + } else { + tagsDisplay.innerHTML = '
No tags
'; + } + + // Update the current recipe object + this.currentRecipe.tags = newTags; + } + + // Hide editor + editor.classList.remove('active'); + tagsContainer.querySelector('.editable-content').classList.remove('hide'); + } + } + + cancelTagsEdit() { + const tagsContainer = document.getElementById('recipeTagsCompact'); + if (tagsContainer) { + // Reset input value + const editor = tagsContainer.querySelector('#recipeTagsEditor'); + const input = editor.querySelector('input'); + input.value = this.currentRecipe.tags ? this.currentRecipe.tags.join(', ') : ''; + + // Hide editor + editor.classList.remove('active'); + tagsContainer.querySelector('.editable-content').classList.remove('hide'); + } + } + + // Update recipe metadata on the server + async updateRecipeMetadata(updates) { + try { + const response = await fetch(`/api/recipe/${this.recipeId}/update`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(updates) + }); + + const data = await response.json(); + + if (data.success) { + // 显示保存成功的提示 + if (updates.title) { + showToast('Recipe name updated successfully', 'success'); + } else if (updates.tags) { + showToast('Recipe tags updated successfully', 'success'); + } else { + showToast('Recipe updated successfully', 'success'); + } + + // 更新当前recipe对象的属性 + Object.assign(this.currentRecipe, updates); + + // 确保这个更新也传播到卡片视图 + // 尝试找到可能显示这个recipe的卡片并更新它 + try { + const recipeCards = document.querySelectorAll('.recipe-card'); + recipeCards.forEach(card => { + if (card.dataset.recipeId === this.recipeId) { + // 更新卡片标题 + if (updates.title) { + const titleElement = card.querySelector('.recipe-title'); + if (titleElement) { + titleElement.textContent = updates.title; + } + } + + // 更新卡片标签 + if (updates.tags) { + const tagsElement = card.querySelector('.recipe-tags'); + if (tagsElement) { + if (updates.tags.length > 0) { + tagsElement.innerHTML = updates.tags.map( + tag => `
${tag}
` + ).join(''); + } else { + tagsElement.innerHTML = ''; + } + } + } + } + }); + } catch (err) { + console.log("Non-critical error updating recipe cards:", err); + } + + // 重要:强制刷新recipes列表,确保从服务器获取最新数据 + try { + if (window.recipeManager && typeof window.recipeManager.loadRecipes === 'function') { + // 异步刷新recipes列表,不阻塞用户界面 + setTimeout(() => { + window.recipeManager.loadRecipes(true); + }, 500); + } + } catch (err) { + console.log("Error refreshing recipes list:", err); + } + } else { + showToast(`Failed to update recipe: ${data.error}`, 'error'); + } + } catch (error) { + console.error('Error updating recipe:', error); + showToast(`Error updating recipe: ${error.message}`, 'error'); + } + } + // Setup copy buttons for prompts and recipe syntax setupCopyButtons() { const copyPromptBtn = document.getElementById('copyPromptBtn');