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 {
` :
`
`;
+ // 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');