diff --git a/locales/de.json b/locales/de.json index 965faff9..5d7c063d 100644 --- a/locales/de.json +++ b/locales/de.json @@ -1510,6 +1510,9 @@ "nameUpdated": "Rezeptname erfolgreich aktualisiert", "tagsUpdated": "Rezept-Tags erfolgreich aktualisiert", "sourceUrlUpdated": "Quell-URL erfolgreich aktualisiert", + "promptUpdated": "Prompt erfolgreich aktualisiert", + "negativePromptUpdated": "Negativer Prompt erfolgreich aktualisiert", + "promptEditorHint": "Drücken Sie Enter zum Speichern, Shift+Enter für neue Zeile", "noRecipeId": "Keine Rezept-ID verfügbar", "sendToWorkflowFailed": "Fehler beim Senden des Rezepts an den Workflow: {message}", "copyFailed": "Fehler beim Kopieren der Rezept-Syntax: {message}", diff --git a/locales/en.json b/locales/en.json index c20a97a0..c2fc9c65 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1510,6 +1510,9 @@ "nameUpdated": "Recipe name updated successfully", "tagsUpdated": "Recipe tags updated successfully", "sourceUrlUpdated": "Source URL updated successfully", + "promptUpdated": "Prompt updated successfully", + "negativePromptUpdated": "Negative prompt updated successfully", + "promptEditorHint": "Press Enter to save, Shift+Enter for new line", "noRecipeId": "No recipe ID available", "sendToWorkflowFailed": "Failed to send recipe to workflow: {message}", "copyFailed": "Error copying recipe syntax: {message}", diff --git a/locales/es.json b/locales/es.json index 8ecbd480..2c0224eb 100644 --- a/locales/es.json +++ b/locales/es.json @@ -1510,6 +1510,9 @@ "nameUpdated": "Nombre de receta actualizado exitosamente", "tagsUpdated": "Etiquetas de receta actualizadas exitosamente", "sourceUrlUpdated": "URL de origen actualizada exitosamente", + "promptUpdated": "Prompt actualizado exitosamente", + "negativePromptUpdated": "Prompt negativo actualizado exitosamente", + "promptEditorHint": "Presiona Enter para guardar, Shift+Enter para nueva línea", "noRecipeId": "No hay ID de receta disponible", "sendToWorkflowFailed": "Error al enviar la receta al flujo de trabajo: {message}", "copyFailed": "Error copiando sintaxis de receta: {message}", diff --git a/locales/fr.json b/locales/fr.json index 74fd1c45..8bccc171 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -1510,6 +1510,9 @@ "nameUpdated": "Nom de la recipe mis à jour avec succès", "tagsUpdated": "Tags de la recipe mis à jour avec succès", "sourceUrlUpdated": "URL source mise à jour avec succès", + "promptUpdated": "Prompt mis à jour avec succès", + "negativePromptUpdated": "Prompt négatif mis à jour avec succès", + "promptEditorHint": "Appuyez sur Entrée pour sauvegarder, Maj+Entrée pour nouvelle ligne", "noRecipeId": "Aucun ID de recipe disponible", "sendToWorkflowFailed": "Échec de l'envoi de la recette vers le workflow : {message}", "copyFailed": "Erreur lors de la copie de la syntaxe de la recipe : {message}", diff --git a/locales/he.json b/locales/he.json index bcb64af9..da09c0c3 100644 --- a/locales/he.json +++ b/locales/he.json @@ -1510,6 +1510,9 @@ "nameUpdated": "שם המתכון עודכן בהצלחה", "tagsUpdated": "תגיות המתכון עודכנו בהצלחה", "sourceUrlUpdated": "כתובת ה-URL המקורית עודכנה בהצלחה", + "promptUpdated": "הפרומפט עודכן בהצלחה", + "negativePromptUpdated": "הפרומפט השלילי עודכן בהצלחה", + "promptEditorHint": "לחץ Enter לשמירה, Shift+Enter לשורה חדשה", "noRecipeId": "אין מזהה מתכון זמין", "sendToWorkflowFailed": "נכשל שליחת המתכון ל-workflow: {message}", "copyFailed": "שגיאה בהעתקת תחביר המתכון: {message}", diff --git a/locales/ja.json b/locales/ja.json index b1dcf57c..6c6b2e79 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -1510,6 +1510,9 @@ "nameUpdated": "レシピ名が正常に更新されました", "tagsUpdated": "レシピタグが正常に更新されました", "sourceUrlUpdated": "ソースURLが正常に更新されました", + "promptUpdated": "プロンプトが正常に更新されました", + "negativePromptUpdated": "ネガティブプロンプトが正常に更新されました", + "promptEditorHint": "Enterキーで保存、Shift+Enterで改行", "noRecipeId": "レシピIDが利用できません", "sendToWorkflowFailed": "ワークフローへのレシピ送信に失敗しました:{message}", "copyFailed": "レシピ構文のコピーエラー:{message}", diff --git a/locales/ko.json b/locales/ko.json index 4e7ddf76..717700c7 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -1510,6 +1510,9 @@ "nameUpdated": "레시피 이름이 성공적으로 업데이트되었습니다", "tagsUpdated": "레시피 태그가 성공적으로 업데이트되었습니다", "sourceUrlUpdated": "소스 URL이 성공적으로 업데이트되었습니다", + "promptUpdated": "프롬프트가 성공적으로 업데이트되었습니다", + "negativePromptUpdated": "네거티브 프롬프트가 성공적으로 업데이트되었습니다", + "promptEditorHint": "Enter 키를 눌러 저장, Shift+Enter로 새 줄", "noRecipeId": "사용 가능한 레시피 ID가 없습니다", "sendToWorkflowFailed": "워크플로우에 레시피 보내기 실패: {message}", "copyFailed": "레시피 문법 복사 오류: {message}", diff --git a/locales/ru.json b/locales/ru.json index 12a40f87..920489d4 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -1510,6 +1510,9 @@ "nameUpdated": "Название рецепта успешно обновлено", "tagsUpdated": "Теги рецепта успешно обновлены", "sourceUrlUpdated": "Исходный URL успешно обновлен", + "promptUpdated": "Промпт успешно обновлён", + "negativePromptUpdated": "Негативный промпт успешно обновлён", + "promptEditorHint": "Нажмите Enter для сохранения, Shift+Enter для новой строки", "noRecipeId": "ID рецепта недоступен", "sendToWorkflowFailed": "Не удалось отправить рецепт в рабочий процесс: {message}", "copyFailed": "Ошибка копирования синтаксиса рецепта: {message}", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index b72e7a28..6ccfe49e 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -1510,6 +1510,9 @@ "nameUpdated": "配方名称更新成功", "tagsUpdated": "配方标签更新成功", "sourceUrlUpdated": "来源 URL 更新成功", + "promptUpdated": "提示词更新成功", + "negativePromptUpdated": "负面提示词更新成功", + "promptEditorHint": "按 Enter 保存,Shift+Enter 换行", "noRecipeId": "无配方 ID", "sendToWorkflowFailed": "发送配方到工作流失败:{message}", "copyFailed": "复制配方语法出错:{message}", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index ee284646..9a7fe49f 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -1510,6 +1510,9 @@ "nameUpdated": "配方名稱已更新", "tagsUpdated": "配方標籤已更新", "sourceUrlUpdated": "來源網址已更新", + "promptUpdated": "提示詞更新成功", + "negativePromptUpdated": "負面提示詞更新成功", + "promptEditorHint": "按 Enter 儲存,Shift+Enter 換行", "noRecipeId": "無配方 ID", "sendToWorkflowFailed": "傳送配方到工作流失敗:{message}", "copyFailed": "複製配方語法錯誤:{message}", diff --git a/py/services/recipes/persistence_service.py b/py/services/recipes/persistence_service.py index e1c7ae15..be307f3b 100644 --- a/py/services/recipes/persistence_service.py +++ b/py/services/recipes/persistence_service.py @@ -173,11 +173,23 @@ class RecipePersistenceService: async def update_recipe(self, *, recipe_scanner, recipe_id: str, updates: dict[str, Any]) -> PersistenceResult: """Update persisted metadata for a recipe.""" - if not any(key in updates for key in ("title", "tags", "source_path", "preview_nsfw_level", "favorite")): + allowed_fields = ( + "title", + "tags", + "source_path", + "preview_nsfw_level", + "favorite", + "gen_params", + ) + + if not any(key in updates for key in allowed_fields): raise RecipeValidationError( - "At least one field to update must be provided (title or tags or source_path or preview_nsfw_level or favorite)" + "At least one field to update must be provided (title or tags or source_path or preview_nsfw_level or favorite or gen_params)" ) + if "gen_params" in updates and not isinstance(updates["gen_params"], dict): + raise RecipeValidationError("gen_params must be an object") + success = await recipe_scanner.update_recipe_metadata(recipe_id, updates) if not success: raise RecipeNotFoundError("Recipe not found or update failed") diff --git a/static/css/components/recipe-modal.css b/static/css/components/recipe-modal.css index 07e9240f..32dcfd4c 100644 --- a/static/css/components/recipe-modal.css +++ b/static/css/components/recipe-modal.css @@ -424,6 +424,7 @@ display: flex; justify-content: space-between; align-items: center; + gap: 8px; } .param-header label { @@ -431,7 +432,14 @@ color: var(--text-color); } -.copy-btn { +.param-actions { + display: flex; + align-items: center; + gap: 4px; +} + +.copy-btn, +.edit-btn { background: none; border: none; color: var(--text-color); @@ -442,7 +450,8 @@ transition: all 0.2s; } -.copy-btn:hover { +.copy-btn:hover, +.edit-btn:hover { opacity: 1; background: var(--lora-surface); } @@ -461,6 +470,48 @@ word-break: break-word; } +.param-content.hide { + display: none; +} + +.param-content.is-placeholder { + color: color-mix(in oklch, var(--text-color), transparent 35%); + font-style: italic; +} + +.param-editor { + display: none; + flex-direction: column; + gap: 10px; +} + +.param-editor.active { + display: flex; +} + +.param-textarea { + width: 100%; + max-width: 100%; + min-height: 140px; + resize: vertical; + background: var(--bg-color); + border: 1px solid var(--lora-border); + border-radius: var(--border-radius-xs); + padding: 10px 12px; + font-size: 0.9em; + line-height: 1.5; + color: var(--text-color); + font-family: inherit; + box-sizing: border-box; + overflow-x: hidden; +} + +.param-editor-hint { + font-size: 0.78em; + line-height: 1.4; + color: color-mix(in oklch, var(--text-color), transparent 35%); +} + /* Other Parameters */ .other-params { display: flex; diff --git a/static/js/components/RecipeModal.js b/static/js/components/RecipeModal.js index 27a0aad7..d0c94722 100644 --- a/static/js/components/RecipeModal.js +++ b/static/js/components/RecipeModal.js @@ -9,11 +9,13 @@ import { MODEL_TYPES } from '../api/apiConfig.js'; class RecipeModal { constructor() { + this.promptEditorState = {}; this.init(); } init() { this.setupCopyButtons(); + this.setupPromptEditors(); // Set up tooltip positioning handlers after DOM is ready document.addEventListener('DOMContentLoaded', () => { this.setupTooltipPositioning(); @@ -87,6 +89,7 @@ class RecipeModal { showRecipeDetails(recipe) { // Store the full recipe for editing this.currentRecipe = recipe; + this.resetPromptEditors(); // Set modal title with edit icon const modalTitle = document.getElementById('recipeModalTitle'); @@ -300,20 +303,19 @@ class RecipeModal { const promptElement = document.getElementById('recipePrompt'); const negativePromptElement = document.getElementById('recipeNegativePrompt'); const otherParamsElement = document.getElementById('recipeOtherParams'); + const promptInput = document.getElementById('recipePromptInput'); + const negativePromptInput = document.getElementById('recipeNegativePromptInput'); if (recipe.gen_params) { - // Set prompt - if (promptElement && recipe.gen_params.prompt) { - promptElement.textContent = recipe.gen_params.prompt; - } else if (promptElement) { - promptElement.textContent = 'No prompt information available'; + this.renderPromptContent(promptElement, recipe.gen_params.prompt, 'No prompt information available'); + this.renderPromptContent(negativePromptElement, recipe.gen_params.negative_prompt, 'No negative prompt information available'); + + if (promptInput) { + promptInput.value = recipe.gen_params.prompt || ''; } - // Set negative prompt - if (negativePromptElement && recipe.gen_params.negative_prompt) { - negativePromptElement.textContent = recipe.gen_params.negative_prompt; - } else if (negativePromptElement) { - negativePromptElement.textContent = 'No negative prompt information available'; + if (negativePromptInput) { + negativePromptInput.value = recipe.gen_params.negative_prompt || ''; } // Set other parameters @@ -343,8 +345,10 @@ class RecipeModal { } } else { // No generation parameters available - if (promptElement) promptElement.textContent = 'No prompt information available'; - if (negativePromptElement) promptElement.textContent = 'No negative prompt information available'; + this.renderPromptContent(promptElement, '', 'No prompt information available'); + this.renderPromptContent(negativePromptElement, '', 'No negative prompt information available'); + if (promptInput) promptInput.value = ''; + if (negativePromptInput) negativePromptInput.value = ''; if (otherParamsElement) otherParamsElement.innerHTML = '