mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-04-02 10:48:51 -03:00
feat(recipe): add editable prompts in recipe modal (#869)
This commit is contained in:
@@ -1510,6 +1510,9 @@
|
|||||||
"nameUpdated": "Rezeptname erfolgreich aktualisiert",
|
"nameUpdated": "Rezeptname erfolgreich aktualisiert",
|
||||||
"tagsUpdated": "Rezept-Tags erfolgreich aktualisiert",
|
"tagsUpdated": "Rezept-Tags erfolgreich aktualisiert",
|
||||||
"sourceUrlUpdated": "Quell-URL 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",
|
"noRecipeId": "Keine Rezept-ID verfügbar",
|
||||||
"sendToWorkflowFailed": "Fehler beim Senden des Rezepts an den Workflow: {message}",
|
"sendToWorkflowFailed": "Fehler beim Senden des Rezepts an den Workflow: {message}",
|
||||||
"copyFailed": "Fehler beim Kopieren der Rezept-Syntax: {message}",
|
"copyFailed": "Fehler beim Kopieren der Rezept-Syntax: {message}",
|
||||||
|
|||||||
@@ -1510,6 +1510,9 @@
|
|||||||
"nameUpdated": "Recipe name updated successfully",
|
"nameUpdated": "Recipe name updated successfully",
|
||||||
"tagsUpdated": "Recipe tags updated successfully",
|
"tagsUpdated": "Recipe tags updated successfully",
|
||||||
"sourceUrlUpdated": "Source URL 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",
|
"noRecipeId": "No recipe ID available",
|
||||||
"sendToWorkflowFailed": "Failed to send recipe to workflow: {message}",
|
"sendToWorkflowFailed": "Failed to send recipe to workflow: {message}",
|
||||||
"copyFailed": "Error copying recipe syntax: {message}",
|
"copyFailed": "Error copying recipe syntax: {message}",
|
||||||
|
|||||||
@@ -1510,6 +1510,9 @@
|
|||||||
"nameUpdated": "Nombre de receta actualizado exitosamente",
|
"nameUpdated": "Nombre de receta actualizado exitosamente",
|
||||||
"tagsUpdated": "Etiquetas de receta actualizadas exitosamente",
|
"tagsUpdated": "Etiquetas de receta actualizadas exitosamente",
|
||||||
"sourceUrlUpdated": "URL de origen actualizada 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",
|
"noRecipeId": "No hay ID de receta disponible",
|
||||||
"sendToWorkflowFailed": "Error al enviar la receta al flujo de trabajo: {message}",
|
"sendToWorkflowFailed": "Error al enviar la receta al flujo de trabajo: {message}",
|
||||||
"copyFailed": "Error copiando sintaxis de receta: {message}",
|
"copyFailed": "Error copiando sintaxis de receta: {message}",
|
||||||
|
|||||||
@@ -1510,6 +1510,9 @@
|
|||||||
"nameUpdated": "Nom de la recipe mis à jour avec succès",
|
"nameUpdated": "Nom de la recipe mis à jour avec succès",
|
||||||
"tagsUpdated": "Tags 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",
|
"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",
|
"noRecipeId": "Aucun ID de recipe disponible",
|
||||||
"sendToWorkflowFailed": "Échec de l'envoi de la recette vers le workflow : {message}",
|
"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}",
|
"copyFailed": "Erreur lors de la copie de la syntaxe de la recipe : {message}",
|
||||||
|
|||||||
@@ -1510,6 +1510,9 @@
|
|||||||
"nameUpdated": "שם המתכון עודכן בהצלחה",
|
"nameUpdated": "שם המתכון עודכן בהצלחה",
|
||||||
"tagsUpdated": "תגיות המתכון עודכנו בהצלחה",
|
"tagsUpdated": "תגיות המתכון עודכנו בהצלחה",
|
||||||
"sourceUrlUpdated": "כתובת ה-URL המקורית עודכנה בהצלחה",
|
"sourceUrlUpdated": "כתובת ה-URL המקורית עודכנה בהצלחה",
|
||||||
|
"promptUpdated": "הפרומפט עודכן בהצלחה",
|
||||||
|
"negativePromptUpdated": "הפרומפט השלילי עודכן בהצלחה",
|
||||||
|
"promptEditorHint": "לחץ Enter לשמירה, Shift+Enter לשורה חדשה",
|
||||||
"noRecipeId": "אין מזהה מתכון זמין",
|
"noRecipeId": "אין מזהה מתכון זמין",
|
||||||
"sendToWorkflowFailed": "נכשל שליחת המתכון ל-workflow: {message}",
|
"sendToWorkflowFailed": "נכשל שליחת המתכון ל-workflow: {message}",
|
||||||
"copyFailed": "שגיאה בהעתקת תחביר המתכון: {message}",
|
"copyFailed": "שגיאה בהעתקת תחביר המתכון: {message}",
|
||||||
|
|||||||
@@ -1510,6 +1510,9 @@
|
|||||||
"nameUpdated": "レシピ名が正常に更新されました",
|
"nameUpdated": "レシピ名が正常に更新されました",
|
||||||
"tagsUpdated": "レシピタグが正常に更新されました",
|
"tagsUpdated": "レシピタグが正常に更新されました",
|
||||||
"sourceUrlUpdated": "ソースURLが正常に更新されました",
|
"sourceUrlUpdated": "ソースURLが正常に更新されました",
|
||||||
|
"promptUpdated": "プロンプトが正常に更新されました",
|
||||||
|
"negativePromptUpdated": "ネガティブプロンプトが正常に更新されました",
|
||||||
|
"promptEditorHint": "Enterキーで保存、Shift+Enterで改行",
|
||||||
"noRecipeId": "レシピIDが利用できません",
|
"noRecipeId": "レシピIDが利用できません",
|
||||||
"sendToWorkflowFailed": "ワークフローへのレシピ送信に失敗しました:{message}",
|
"sendToWorkflowFailed": "ワークフローへのレシピ送信に失敗しました:{message}",
|
||||||
"copyFailed": "レシピ構文のコピーエラー:{message}",
|
"copyFailed": "レシピ構文のコピーエラー:{message}",
|
||||||
|
|||||||
@@ -1510,6 +1510,9 @@
|
|||||||
"nameUpdated": "레시피 이름이 성공적으로 업데이트되었습니다",
|
"nameUpdated": "레시피 이름이 성공적으로 업데이트되었습니다",
|
||||||
"tagsUpdated": "레시피 태그가 성공적으로 업데이트되었습니다",
|
"tagsUpdated": "레시피 태그가 성공적으로 업데이트되었습니다",
|
||||||
"sourceUrlUpdated": "소스 URL이 성공적으로 업데이트되었습니다",
|
"sourceUrlUpdated": "소스 URL이 성공적으로 업데이트되었습니다",
|
||||||
|
"promptUpdated": "프롬프트가 성공적으로 업데이트되었습니다",
|
||||||
|
"negativePromptUpdated": "네거티브 프롬프트가 성공적으로 업데이트되었습니다",
|
||||||
|
"promptEditorHint": "Enter 키를 눌러 저장, Shift+Enter로 새 줄",
|
||||||
"noRecipeId": "사용 가능한 레시피 ID가 없습니다",
|
"noRecipeId": "사용 가능한 레시피 ID가 없습니다",
|
||||||
"sendToWorkflowFailed": "워크플로우에 레시피 보내기 실패: {message}",
|
"sendToWorkflowFailed": "워크플로우에 레시피 보내기 실패: {message}",
|
||||||
"copyFailed": "레시피 문법 복사 오류: {message}",
|
"copyFailed": "레시피 문법 복사 오류: {message}",
|
||||||
|
|||||||
@@ -1510,6 +1510,9 @@
|
|||||||
"nameUpdated": "Название рецепта успешно обновлено",
|
"nameUpdated": "Название рецепта успешно обновлено",
|
||||||
"tagsUpdated": "Теги рецепта успешно обновлены",
|
"tagsUpdated": "Теги рецепта успешно обновлены",
|
||||||
"sourceUrlUpdated": "Исходный URL успешно обновлен",
|
"sourceUrlUpdated": "Исходный URL успешно обновлен",
|
||||||
|
"promptUpdated": "Промпт успешно обновлён",
|
||||||
|
"negativePromptUpdated": "Негативный промпт успешно обновлён",
|
||||||
|
"promptEditorHint": "Нажмите Enter для сохранения, Shift+Enter для новой строки",
|
||||||
"noRecipeId": "ID рецепта недоступен",
|
"noRecipeId": "ID рецепта недоступен",
|
||||||
"sendToWorkflowFailed": "Не удалось отправить рецепт в рабочий процесс: {message}",
|
"sendToWorkflowFailed": "Не удалось отправить рецепт в рабочий процесс: {message}",
|
||||||
"copyFailed": "Ошибка копирования синтаксиса рецепта: {message}",
|
"copyFailed": "Ошибка копирования синтаксиса рецепта: {message}",
|
||||||
|
|||||||
@@ -1510,6 +1510,9 @@
|
|||||||
"nameUpdated": "配方名称更新成功",
|
"nameUpdated": "配方名称更新成功",
|
||||||
"tagsUpdated": "配方标签更新成功",
|
"tagsUpdated": "配方标签更新成功",
|
||||||
"sourceUrlUpdated": "来源 URL 更新成功",
|
"sourceUrlUpdated": "来源 URL 更新成功",
|
||||||
|
"promptUpdated": "提示词更新成功",
|
||||||
|
"negativePromptUpdated": "负面提示词更新成功",
|
||||||
|
"promptEditorHint": "按 Enter 保存,Shift+Enter 换行",
|
||||||
"noRecipeId": "无配方 ID",
|
"noRecipeId": "无配方 ID",
|
||||||
"sendToWorkflowFailed": "发送配方到工作流失败:{message}",
|
"sendToWorkflowFailed": "发送配方到工作流失败:{message}",
|
||||||
"copyFailed": "复制配方语法出错:{message}",
|
"copyFailed": "复制配方语法出错:{message}",
|
||||||
|
|||||||
@@ -1510,6 +1510,9 @@
|
|||||||
"nameUpdated": "配方名稱已更新",
|
"nameUpdated": "配方名稱已更新",
|
||||||
"tagsUpdated": "配方標籤已更新",
|
"tagsUpdated": "配方標籤已更新",
|
||||||
"sourceUrlUpdated": "來源網址已更新",
|
"sourceUrlUpdated": "來源網址已更新",
|
||||||
|
"promptUpdated": "提示詞更新成功",
|
||||||
|
"negativePromptUpdated": "負面提示詞更新成功",
|
||||||
|
"promptEditorHint": "按 Enter 儲存,Shift+Enter 換行",
|
||||||
"noRecipeId": "無配方 ID",
|
"noRecipeId": "無配方 ID",
|
||||||
"sendToWorkflowFailed": "傳送配方到工作流失敗:{message}",
|
"sendToWorkflowFailed": "傳送配方到工作流失敗:{message}",
|
||||||
"copyFailed": "複製配方語法錯誤:{message}",
|
"copyFailed": "複製配方語法錯誤:{message}",
|
||||||
|
|||||||
@@ -173,11 +173,23 @@ class RecipePersistenceService:
|
|||||||
async def update_recipe(self, *, recipe_scanner, recipe_id: str, updates: dict[str, Any]) -> PersistenceResult:
|
async def update_recipe(self, *, recipe_scanner, recipe_id: str, updates: dict[str, Any]) -> PersistenceResult:
|
||||||
"""Update persisted metadata for a recipe."""
|
"""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(
|
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)
|
success = await recipe_scanner.update_recipe_metadata(recipe_id, updates)
|
||||||
if not success:
|
if not success:
|
||||||
raise RecipeNotFoundError("Recipe not found or update failed")
|
raise RecipeNotFoundError("Recipe not found or update failed")
|
||||||
|
|||||||
@@ -424,6 +424,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.param-header label {
|
.param-header label {
|
||||||
@@ -431,7 +432,14 @@
|
|||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.copy-btn {
|
.param-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn,
|
||||||
|
.edit-btn {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
@@ -442,7 +450,8 @@
|
|||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.copy-btn:hover {
|
.copy-btn:hover,
|
||||||
|
.edit-btn:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
background: var(--lora-surface);
|
background: var(--lora-surface);
|
||||||
}
|
}
|
||||||
@@ -461,6 +470,48 @@
|
|||||||
word-break: break-word;
|
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 Parameters */
|
||||||
.other-params {
|
.other-params {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ import { MODEL_TYPES } from '../api/apiConfig.js';
|
|||||||
|
|
||||||
class RecipeModal {
|
class RecipeModal {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
this.promptEditorState = {};
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.setupCopyButtons();
|
this.setupCopyButtons();
|
||||||
|
this.setupPromptEditors();
|
||||||
// Set up tooltip positioning handlers after DOM is ready
|
// Set up tooltip positioning handlers after DOM is ready
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
this.setupTooltipPositioning();
|
this.setupTooltipPositioning();
|
||||||
@@ -87,6 +89,7 @@ class RecipeModal {
|
|||||||
showRecipeDetails(recipe) {
|
showRecipeDetails(recipe) {
|
||||||
// Store the full recipe for editing
|
// Store the full recipe for editing
|
||||||
this.currentRecipe = recipe;
|
this.currentRecipe = recipe;
|
||||||
|
this.resetPromptEditors();
|
||||||
|
|
||||||
// Set modal title with edit icon
|
// Set modal title with edit icon
|
||||||
const modalTitle = document.getElementById('recipeModalTitle');
|
const modalTitle = document.getElementById('recipeModalTitle');
|
||||||
@@ -300,20 +303,19 @@ class RecipeModal {
|
|||||||
const promptElement = document.getElementById('recipePrompt');
|
const promptElement = document.getElementById('recipePrompt');
|
||||||
const negativePromptElement = document.getElementById('recipeNegativePrompt');
|
const negativePromptElement = document.getElementById('recipeNegativePrompt');
|
||||||
const otherParamsElement = document.getElementById('recipeOtherParams');
|
const otherParamsElement = document.getElementById('recipeOtherParams');
|
||||||
|
const promptInput = document.getElementById('recipePromptInput');
|
||||||
|
const negativePromptInput = document.getElementById('recipeNegativePromptInput');
|
||||||
|
|
||||||
if (recipe.gen_params) {
|
if (recipe.gen_params) {
|
||||||
// Set prompt
|
this.renderPromptContent(promptElement, recipe.gen_params.prompt, 'No prompt information available');
|
||||||
if (promptElement && recipe.gen_params.prompt) {
|
this.renderPromptContent(negativePromptElement, recipe.gen_params.negative_prompt, 'No negative prompt information available');
|
||||||
promptElement.textContent = recipe.gen_params.prompt;
|
|
||||||
} else if (promptElement) {
|
if (promptInput) {
|
||||||
promptElement.textContent = 'No prompt information available';
|
promptInput.value = recipe.gen_params.prompt || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set negative prompt
|
if (negativePromptInput) {
|
||||||
if (negativePromptElement && recipe.gen_params.negative_prompt) {
|
negativePromptInput.value = recipe.gen_params.negative_prompt || '';
|
||||||
negativePromptElement.textContent = recipe.gen_params.negative_prompt;
|
|
||||||
} else if (negativePromptElement) {
|
|
||||||
negativePromptElement.textContent = 'No negative prompt information available';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set other parameters
|
// Set other parameters
|
||||||
@@ -343,8 +345,10 @@ class RecipeModal {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No generation parameters available
|
// No generation parameters available
|
||||||
if (promptElement) promptElement.textContent = 'No prompt information available';
|
this.renderPromptContent(promptElement, '', 'No prompt information available');
|
||||||
if (negativePromptElement) promptElement.textContent = 'No negative prompt information available';
|
this.renderPromptContent(negativePromptElement, '', 'No negative prompt information available');
|
||||||
|
if (promptInput) promptInput.value = '';
|
||||||
|
if (negativePromptInput) negativePromptInput.value = '';
|
||||||
if (otherParamsElement) otherParamsElement.innerHTML = '<div class="no-params">No parameters available</div>';
|
if (otherParamsElement) otherParamsElement.innerHTML = '<div class="no-params">No parameters available</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -711,16 +715,202 @@ class RecipeModal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setupPromptEditors() {
|
||||||
|
const promptConfigs = [
|
||||||
|
{
|
||||||
|
editButtonId: 'editPromptBtn',
|
||||||
|
contentId: 'recipePrompt',
|
||||||
|
editorId: 'recipePromptEditor',
|
||||||
|
inputId: 'recipePromptInput',
|
||||||
|
field: 'prompt',
|
||||||
|
placeholder: 'No prompt information available',
|
||||||
|
successKey: 'toast.recipes.promptUpdated',
|
||||||
|
successFallback: 'Prompt updated successfully',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
editButtonId: 'editNegativePromptBtn',
|
||||||
|
contentId: 'recipeNegativePrompt',
|
||||||
|
editorId: 'recipeNegativePromptEditor',
|
||||||
|
inputId: 'recipeNegativePromptInput',
|
||||||
|
field: 'negative_prompt',
|
||||||
|
placeholder: 'No negative prompt information available',
|
||||||
|
successKey: 'toast.recipes.negativePromptUpdated',
|
||||||
|
successFallback: 'Negative prompt updated successfully',
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
promptConfigs.forEach((config) => {
|
||||||
|
const editButton = document.getElementById(config.editButtonId);
|
||||||
|
const input = document.getElementById(config.inputId);
|
||||||
|
|
||||||
|
if (editButton) {
|
||||||
|
editButton.addEventListener('click', () => this.showPromptEditor(config));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input) {
|
||||||
|
input.addEventListener('keydown', (event) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
this.cancelPromptEdit(config);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'Enter' && !event.shiftKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
this.promptEditorState[config.field] = {
|
||||||
|
...(this.promptEditorState[config.field] || {}),
|
||||||
|
skipBlurSave: true,
|
||||||
|
};
|
||||||
|
this.savePromptEdit(config);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
input.addEventListener('blur', () => {
|
||||||
|
const promptState = this.promptEditorState[config.field] || {};
|
||||||
|
if (promptState.skipBlurSave) {
|
||||||
|
this.promptEditorState[config.field] = {
|
||||||
|
...promptState,
|
||||||
|
skipBlurSave: false,
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.savePromptEdit(config);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPromptContent(element, value, placeholder) {
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = value || '';
|
||||||
|
if (text) {
|
||||||
|
element.textContent = text;
|
||||||
|
element.classList.remove('is-placeholder');
|
||||||
|
} else {
|
||||||
|
element.textContent = placeholder;
|
||||||
|
element.classList.add('is-placeholder');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resetPromptEditors() {
|
||||||
|
this.hidePromptEditor({ contentId: 'recipePrompt', editorId: 'recipePromptEditor' });
|
||||||
|
this.hidePromptEditor({ contentId: 'recipeNegativePrompt', editorId: 'recipeNegativePromptEditor' });
|
||||||
|
}
|
||||||
|
|
||||||
|
showPromptEditor(config) {
|
||||||
|
const content = document.getElementById(config.contentId);
|
||||||
|
const editor = document.getElementById(config.editorId);
|
||||||
|
const input = document.getElementById(config.inputId);
|
||||||
|
|
||||||
|
if (!content || !editor || !input) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentValue = this.currentRecipe?.gen_params?.[config.field] || '';
|
||||||
|
input.value = currentValue;
|
||||||
|
this.promptEditorState[config.field] = {
|
||||||
|
initialValue: currentValue,
|
||||||
|
skipBlurSave: false,
|
||||||
|
isSaving: false,
|
||||||
|
};
|
||||||
|
content.classList.add('hide');
|
||||||
|
editor.classList.add('active');
|
||||||
|
input.focus();
|
||||||
|
input.setSelectionRange(input.value.length, input.value.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
async savePromptEdit(config) {
|
||||||
|
const content = document.getElementById(config.contentId);
|
||||||
|
const editor = document.getElementById(config.editorId);
|
||||||
|
const input = document.getElementById(config.inputId);
|
||||||
|
|
||||||
|
if (!content || !editor || !input || !this.currentRecipe) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const promptState = this.promptEditorState[config.field] || {};
|
||||||
|
if (promptState.isSaving) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentGenParams = this.currentRecipe.gen_params || {};
|
||||||
|
const nextValue = input.value.trim() === '' ? '' : input.value;
|
||||||
|
const currentValue = currentGenParams[config.field] || '';
|
||||||
|
|
||||||
|
if (nextValue === currentValue) {
|
||||||
|
this.hidePromptEditor(config);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextGenParams = {
|
||||||
|
...currentGenParams,
|
||||||
|
[config.field]: nextValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.promptEditorState[config.field] = {
|
||||||
|
...promptState,
|
||||||
|
isSaving: true,
|
||||||
|
};
|
||||||
|
await updateRecipeMetadata(this.filePath, { gen_params: nextGenParams });
|
||||||
|
this.currentRecipe.gen_params = nextGenParams;
|
||||||
|
this.renderPromptContent(content, nextValue, config.placeholder);
|
||||||
|
showToast(config.successKey, {}, 'success', config.successFallback);
|
||||||
|
} catch (error) {
|
||||||
|
this.renderPromptContent(content, currentValue, config.placeholder);
|
||||||
|
input.value = currentValue;
|
||||||
|
} finally {
|
||||||
|
this.hidePromptEditor(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelPromptEdit(config) {
|
||||||
|
const input = document.getElementById(config.inputId);
|
||||||
|
if (input) {
|
||||||
|
const initialValue = this.promptEditorState[config.field]?.initialValue;
|
||||||
|
input.value = initialValue ?? (this.currentRecipe?.gen_params?.[config.field] || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hidePromptEditor(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
hidePromptEditor(config) {
|
||||||
|
const content = document.getElementById(config.contentId);
|
||||||
|
const editor = document.getElementById(config.editorId);
|
||||||
|
|
||||||
|
if (content) {
|
||||||
|
content.classList.remove('hide');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editor) {
|
||||||
|
editor.classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
delete this.promptEditorState[config.field];
|
||||||
|
}
|
||||||
|
|
||||||
// Setup source URL handlers
|
// Setup source URL handlers
|
||||||
setupSourceUrlHandlers() {
|
setupSourceUrlHandlers() {
|
||||||
const sourceUrlContainer = document.querySelector('.source-url-container');
|
const sourceUrlContainer = document.querySelector('.source-url-container');
|
||||||
const sourceUrlEditor = document.querySelector('.source-url-editor');
|
const sourceUrlEditor = document.querySelector('.source-url-editor');
|
||||||
|
if (!sourceUrlContainer || !sourceUrlEditor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const sourceUrlText = sourceUrlContainer.querySelector('.source-url-text');
|
const sourceUrlText = sourceUrlContainer.querySelector('.source-url-text');
|
||||||
const sourceUrlEditBtn = sourceUrlContainer.querySelector('.source-url-edit-btn');
|
const sourceUrlEditBtn = sourceUrlContainer.querySelector('.source-url-edit-btn');
|
||||||
const sourceUrlCancelBtn = sourceUrlEditor.querySelector('.source-url-cancel-btn');
|
const sourceUrlCancelBtn = sourceUrlEditor.querySelector('.source-url-cancel-btn');
|
||||||
const sourceUrlSaveBtn = sourceUrlEditor.querySelector('.source-url-save-btn');
|
const sourceUrlSaveBtn = sourceUrlEditor.querySelector('.source-url-save-btn');
|
||||||
const sourceUrlInput = sourceUrlEditor.querySelector('.source-url-input');
|
const sourceUrlInput = sourceUrlEditor.querySelector('.source-url-input');
|
||||||
|
|
||||||
|
if (!sourceUrlText || !sourceUrlEditBtn || !sourceUrlCancelBtn || !sourceUrlSaveBtn || !sourceUrlInput) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Show editor on edit button click
|
// Show editor on edit button click
|
||||||
sourceUrlEditBtn.addEventListener('click', () => {
|
sourceUrlEditBtn.addEventListener('click', () => {
|
||||||
sourceUrlContainer.classList.add('hide');
|
sourceUrlContainer.classList.add('hide');
|
||||||
@@ -782,14 +972,14 @@ class RecipeModal {
|
|||||||
|
|
||||||
if (copyPromptBtn) {
|
if (copyPromptBtn) {
|
||||||
copyPromptBtn.addEventListener('click', () => {
|
copyPromptBtn.addEventListener('click', () => {
|
||||||
const promptText = document.getElementById('recipePrompt').textContent;
|
const promptText = this.currentRecipe?.gen_params?.prompt || '';
|
||||||
this.copyToClipboard(promptText, 'Prompt copied to clipboard');
|
this.copyToClipboard(promptText, 'Prompt copied to clipboard');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (copyNegativePromptBtn) {
|
if (copyNegativePromptBtn) {
|
||||||
copyNegativePromptBtn.addEventListener('click', () => {
|
copyNegativePromptBtn.addEventListener('click', () => {
|
||||||
const negativePromptText = document.getElementById('recipeNegativePrompt').textContent;
|
const negativePromptText = this.currentRecipe?.gen_params?.negative_prompt || '';
|
||||||
this.copyToClipboard(negativePromptText, 'Negative prompt copied to clipboard');
|
this.copyToClipboard(negativePromptText, 'Negative prompt copied to clipboard');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,22 +29,52 @@
|
|||||||
<div class="param-group info-item">
|
<div class="param-group info-item">
|
||||||
<div class="param-header">
|
<div class="param-header">
|
||||||
<label>Prompt</label>
|
<label>Prompt</label>
|
||||||
<button class="copy-btn" id="copyPromptBtn" title="Copy Prompt">
|
<div class="param-actions">
|
||||||
<i class="fas fa-copy"></i>
|
<button class="copy-btn" id="copyPromptBtn" title="Copy Prompt">
|
||||||
</button>
|
<i class="fas fa-copy"></i>
|
||||||
|
</button>
|
||||||
|
<button class="edit-btn" id="editPromptBtn" title="Edit Prompt">
|
||||||
|
<i class="fas fa-pencil-alt"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="param-content" id="recipePrompt"></div>
|
<div class="param-content" id="recipePrompt"></div>
|
||||||
|
<div class="param-editor" id="recipePromptEditor">
|
||||||
|
<textarea
|
||||||
|
class="param-textarea"
|
||||||
|
id="recipePromptInput"
|
||||||
|
placeholder="Enter prompt"
|
||||||
|
></textarea>
|
||||||
|
<div class="param-editor-hint">
|
||||||
|
{{ t('toast.recipes.promptEditorHint') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Negative Prompt -->
|
<!-- Negative Prompt -->
|
||||||
<div class="param-group info-item">
|
<div class="param-group info-item">
|
||||||
<div class="param-header">
|
<div class="param-header">
|
||||||
<label>Negative Prompt</label>
|
<label>Negative Prompt</label>
|
||||||
<button class="copy-btn" id="copyNegativePromptBtn" title="Copy Negative Prompt">
|
<div class="param-actions">
|
||||||
<i class="fas fa-copy"></i>
|
<button class="copy-btn" id="copyNegativePromptBtn" title="Copy Negative Prompt">
|
||||||
</button>
|
<i class="fas fa-copy"></i>
|
||||||
|
</button>
|
||||||
|
<button class="edit-btn" id="editNegativePromptBtn" title="Edit Negative Prompt">
|
||||||
|
<i class="fas fa-pencil-alt"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="param-content" id="recipeNegativePrompt"></div>
|
<div class="param-content" id="recipeNegativePrompt"></div>
|
||||||
|
<div class="param-editor" id="recipeNegativePromptEditor">
|
||||||
|
<textarea
|
||||||
|
class="param-textarea"
|
||||||
|
id="recipeNegativePromptInput"
|
||||||
|
placeholder="Enter negative prompt"
|
||||||
|
></textarea>
|
||||||
|
<div class="param-editor-hint">
|
||||||
|
{{ t('toast.recipes.promptEditorHint') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Other Parameters -->
|
<!-- Other Parameters -->
|
||||||
|
|||||||
@@ -245,16 +245,28 @@ describe('Interaction-level regression coverage', () => {
|
|||||||
<div class="param-group info-item">
|
<div class="param-group info-item">
|
||||||
<div class="param-header">
|
<div class="param-header">
|
||||||
<label>Prompt</label>
|
<label>Prompt</label>
|
||||||
<button class="copy-btn" id="copyPromptBtn" title="Copy Prompt"><i class="fas fa-copy"></i></button>
|
<div class="param-actions">
|
||||||
|
<button class="copy-btn" id="copyPromptBtn" title="Copy Prompt"><i class="fas fa-copy"></i></button>
|
||||||
|
<button class="edit-btn" id="editPromptBtn" title="Edit Prompt"><i class="fas fa-pencil-alt"></i></button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="param-content" id="recipePrompt"></div>
|
<div class="param-content" id="recipePrompt"></div>
|
||||||
|
<div class="param-editor" id="recipePromptEditor">
|
||||||
|
<textarea class="param-textarea" id="recipePromptInput"></textarea>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="param-group info-item">
|
<div class="param-group info-item">
|
||||||
<div class="param-header">
|
<div class="param-header">
|
||||||
<label>Negative Prompt</label>
|
<label>Negative Prompt</label>
|
||||||
<button class="copy-btn" id="copyNegativePromptBtn" title="Copy Negative Prompt"><i class="fas fa-copy"></i></button>
|
<div class="param-actions">
|
||||||
|
<button class="copy-btn" id="copyNegativePromptBtn" title="Copy Negative Prompt"><i class="fas fa-copy"></i></button>
|
||||||
|
<button class="edit-btn" id="editNegativePromptBtn" title="Edit Negative Prompt"><i class="fas fa-pencil-alt"></i></button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="param-content" id="recipeNegativePrompt"></div>
|
<div class="param-content" id="recipeNegativePrompt"></div>
|
||||||
|
<div class="param-editor" id="recipeNegativePromptEditor">
|
||||||
|
<textarea class="param-textarea" id="recipeNegativePromptInput"></textarea>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="other-params" id="recipeOtherParams"></div>
|
<div class="other-params" id="recipeOtherParams"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -324,6 +336,208 @@ describe('Interaction-level regression coverage', () => {
|
|||||||
expect(recipeModal.currentRecipe.title).toBe('Updated Title');
|
expect(recipeModal.currentRecipe.title).toBe('Updated Title');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('saves prompt edits on Enter while preserving Shift+Enter for new lines', async () => {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<div id="recipeModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<header class="recipe-modal-header">
|
||||||
|
<h2 id="recipeModalTitle">Recipe Details</h2>
|
||||||
|
<div class="recipe-tags-container">
|
||||||
|
<div class="recipe-tags-compact" id="recipeTagsCompact"></div>
|
||||||
|
<div class="recipe-tags-tooltip" id="recipeTagsTooltip">
|
||||||
|
<div class="tooltip-content" id="recipeTagsTooltipContent"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="recipe-top-section">
|
||||||
|
<div class="recipe-preview-container" id="recipePreviewContainer">
|
||||||
|
<img id="recipeModalImage" src="" alt="Recipe Preview" class="recipe-preview-media">
|
||||||
|
</div>
|
||||||
|
<div class="info-section recipe-gen-params">
|
||||||
|
<div class="gen-params-container">
|
||||||
|
<div class="param-group info-item">
|
||||||
|
<div class="param-header">
|
||||||
|
<label>Prompt</label>
|
||||||
|
<div class="param-actions">
|
||||||
|
<button class="copy-btn" id="copyPromptBtn" title="Copy Prompt"><i class="fas fa-copy"></i></button>
|
||||||
|
<button class="edit-btn" id="editPromptBtn" title="Edit Prompt"><i class="fas fa-pencil-alt"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="param-content" id="recipePrompt"></div>
|
||||||
|
<div class="param-editor" id="recipePromptEditor">
|
||||||
|
<textarea class="param-textarea" id="recipePromptInput"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="param-group info-item">
|
||||||
|
<div class="param-header">
|
||||||
|
<label>Negative Prompt</label>
|
||||||
|
<div class="param-actions">
|
||||||
|
<button class="copy-btn" id="copyNegativePromptBtn" title="Copy Negative Prompt"><i class="fas fa-copy"></i></button>
|
||||||
|
<button class="edit-btn" id="editNegativePromptBtn" title="Edit Negative Prompt"><i class="fas fa-pencil-alt"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="param-content" id="recipeNegativePrompt"></div>
|
||||||
|
<div class="param-editor" id="recipeNegativePromptEditor">
|
||||||
|
<textarea class="param-textarea" id="recipeNegativePromptInput"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="other-params" id="recipeOtherParams"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-section recipe-bottom-section">
|
||||||
|
<div class="recipe-section-header">
|
||||||
|
<h3>Resources</h3>
|
||||||
|
<div class="recipe-section-actions">
|
||||||
|
<span id="recipeLorasCount"><i class="fas fa-layer-group"></i> 0 LoRAs</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="recipe-loras-list" id="recipeLorasList"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const { RecipeModal } = await import('../../../static/js/components/RecipeModal.js');
|
||||||
|
const recipeModal = new RecipeModal();
|
||||||
|
|
||||||
|
recipeModal.showRecipeDetails({
|
||||||
|
id: 'recipe-2',
|
||||||
|
file_path: '/recipes/prompt.json',
|
||||||
|
title: 'Prompt Recipe',
|
||||||
|
tags: [],
|
||||||
|
file_url: '',
|
||||||
|
preview_url: '',
|
||||||
|
source_path: '',
|
||||||
|
gen_params: {
|
||||||
|
prompt: 'old prompt',
|
||||||
|
negative_prompt: 'keep negative',
|
||||||
|
steps: 30,
|
||||||
|
cfg_scale: 7,
|
||||||
|
},
|
||||||
|
loras: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('editPromptBtn').click();
|
||||||
|
const textarea = document.getElementById('recipePromptInput');
|
||||||
|
textarea.value = 'new prompt text';
|
||||||
|
textarea.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', shiftKey: true, bubbles: true }));
|
||||||
|
await flushAsyncTasks();
|
||||||
|
|
||||||
|
expect(updateRecipeMetadataMock).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
textarea.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
||||||
|
await updateRecipeMetadataMock.mock.results[0].value;
|
||||||
|
await flushAsyncTasks();
|
||||||
|
|
||||||
|
expect(updateRecipeMetadataMock).toHaveBeenCalledWith('/recipes/prompt.json', {
|
||||||
|
gen_params: {
|
||||||
|
prompt: 'new prompt text',
|
||||||
|
negative_prompt: 'keep negative',
|
||||||
|
steps: 30,
|
||||||
|
cfg_scale: 7,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(document.getElementById('recipePrompt').textContent).toBe('new prompt text');
|
||||||
|
expect(recipeModal.currentRecipe.gen_params.prompt).toBe('new prompt text');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cancels negative prompt edits on Escape without saving', async () => {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<div id="recipeModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<header class="recipe-modal-header">
|
||||||
|
<h2 id="recipeModalTitle">Recipe Details</h2>
|
||||||
|
<div class="recipe-tags-container">
|
||||||
|
<div class="recipe-tags-compact" id="recipeTagsCompact"></div>
|
||||||
|
<div class="recipe-tags-tooltip" id="recipeTagsTooltip">
|
||||||
|
<div class="tooltip-content" id="recipeTagsTooltipContent"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="recipe-top-section">
|
||||||
|
<div class="recipe-preview-container" id="recipePreviewContainer">
|
||||||
|
<img id="recipeModalImage" src="" alt="Recipe Preview" class="recipe-preview-media">
|
||||||
|
</div>
|
||||||
|
<div class="info-section recipe-gen-params">
|
||||||
|
<div class="gen-params-container">
|
||||||
|
<div class="param-group info-item">
|
||||||
|
<div class="param-header">
|
||||||
|
<label>Prompt</label>
|
||||||
|
<div class="param-actions">
|
||||||
|
<button class="copy-btn" id="copyPromptBtn" title="Copy Prompt"><i class="fas fa-copy"></i></button>
|
||||||
|
<button class="edit-btn" id="editPromptBtn" title="Edit Prompt"><i class="fas fa-pencil-alt"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="param-content" id="recipePrompt"></div>
|
||||||
|
<div class="param-editor" id="recipePromptEditor">
|
||||||
|
<textarea class="param-textarea" id="recipePromptInput"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="param-group info-item">
|
||||||
|
<div class="param-header">
|
||||||
|
<label>Negative Prompt</label>
|
||||||
|
<div class="param-actions">
|
||||||
|
<button class="copy-btn" id="copyNegativePromptBtn" title="Copy Negative Prompt"><i class="fas fa-copy"></i></button>
|
||||||
|
<button class="edit-btn" id="editNegativePromptBtn" title="Edit Negative Prompt"><i class="fas fa-pencil-alt"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="param-content" id="recipeNegativePrompt"></div>
|
||||||
|
<div class="param-editor" id="recipeNegativePromptEditor">
|
||||||
|
<textarea class="param-textarea" id="recipeNegativePromptInput"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="other-params" id="recipeOtherParams"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-section recipe-bottom-section">
|
||||||
|
<div class="recipe-section-header">
|
||||||
|
<h3>Resources</h3>
|
||||||
|
<div class="recipe-section-actions">
|
||||||
|
<span id="recipeLorasCount"><i class="fas fa-layer-group"></i> 0 LoRAs</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="recipe-loras-list" id="recipeLorasList"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const { RecipeModal } = await import('../../../static/js/components/RecipeModal.js');
|
||||||
|
const recipeModal = new RecipeModal();
|
||||||
|
|
||||||
|
recipeModal.showRecipeDetails({
|
||||||
|
id: 'recipe-3',
|
||||||
|
file_path: '/recipes/negative.json',
|
||||||
|
title: 'Negative Recipe',
|
||||||
|
tags: [],
|
||||||
|
file_url: '',
|
||||||
|
preview_url: '',
|
||||||
|
source_path: '',
|
||||||
|
gen_params: {
|
||||||
|
prompt: '',
|
||||||
|
negative_prompt: 'existing negative',
|
||||||
|
steps: 20,
|
||||||
|
},
|
||||||
|
loras: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('editNegativePromptBtn').click();
|
||||||
|
const textarea = document.getElementById('recipeNegativePromptInput');
|
||||||
|
textarea.value = 'changed negative';
|
||||||
|
textarea.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
||||||
|
|
||||||
|
expect(updateRecipeMetadataMock).not.toHaveBeenCalled();
|
||||||
|
expect(modalManagerMock.closeModal).not.toHaveBeenCalled();
|
||||||
|
expect(document.getElementById('recipeNegativePrompt').textContent).toBe('existing negative');
|
||||||
|
expect(document.getElementById('recipeNegativePromptEditor').classList.contains('active')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it('processes global context menu actions for downloads and cleanup', async () => {
|
it('processes global context menu actions for downloads and cleanup', async () => {
|
||||||
document.body.innerHTML = `
|
document.body.innerHTML = `
|
||||||
<div id="globalContextMenu" class="context-menu">
|
<div id="globalContextMenu" class="context-menu">
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ class StubPersistenceService:
|
|||||||
self.save_calls: List[Dict[str, Any]] = []
|
self.save_calls: List[Dict[str, Any]] = []
|
||||||
self.delete_calls: List[str] = []
|
self.delete_calls: List[str] = []
|
||||||
self.move_calls: List[Dict[str, str]] = []
|
self.move_calls: List[Dict[str, str]] = []
|
||||||
|
self.update_calls: List[Dict[str, Any]] = []
|
||||||
self.save_result = SimpleNamespace(
|
self.save_result = SimpleNamespace(
|
||||||
payload={"success": True, "recipe_id": "stub-id"}, status=200
|
payload={"success": True, "recipe_id": "stub-id"}, status=200
|
||||||
)
|
)
|
||||||
@@ -182,7 +183,14 @@ class StubPersistenceService:
|
|||||||
|
|
||||||
async def update_recipe(
|
async def update_recipe(
|
||||||
self, *, recipe_scanner, recipe_id: str, updates: Dict[str, Any]
|
self, *, recipe_scanner, recipe_id: str, updates: Dict[str, Any]
|
||||||
) -> SimpleNamespace: # pragma: no cover - unused by smoke tests
|
) -> SimpleNamespace:
|
||||||
|
self.update_calls.append(
|
||||||
|
{
|
||||||
|
"recipe_scanner": recipe_scanner,
|
||||||
|
"recipe_id": recipe_id,
|
||||||
|
"updates": updates,
|
||||||
|
}
|
||||||
|
)
|
||||||
return SimpleNamespace(
|
return SimpleNamespace(
|
||||||
payload={"success": True, "recipe_id": recipe_id, "updates": updates},
|
payload={"success": True, "recipe_id": recipe_id, "updates": updates},
|
||||||
status=200,
|
status=200,
|
||||||
@@ -509,6 +517,33 @@ async def test_import_remote_recipe_falls_back_to_request_base_model(
|
|||||||
assert provider_calls == ["77"]
|
assert provider_calls == ["77"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_update_recipe_accepts_gen_params(monkeypatch, tmp_path: Path) -> None:
|
||||||
|
async with recipe_harness(monkeypatch, tmp_path) as harness:
|
||||||
|
payload = {
|
||||||
|
"gen_params": {
|
||||||
|
"prompt": "updated prompt",
|
||||||
|
"negative_prompt": "updated negative",
|
||||||
|
"steps": 30,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await harness.client.put(
|
||||||
|
"/api/lm/recipe/recipe-42/update",
|
||||||
|
json=payload,
|
||||||
|
)
|
||||||
|
data = await response.json()
|
||||||
|
|
||||||
|
assert response.status == 200
|
||||||
|
assert data["success"] is True
|
||||||
|
assert harness.persistence.update_calls == [
|
||||||
|
{
|
||||||
|
"recipe_scanner": harness.scanner,
|
||||||
|
"recipe_id": "recipe-42",
|
||||||
|
"updates": payload,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
async def test_import_remote_video_recipe(monkeypatch, tmp_path: Path) -> None:
|
async def test_import_remote_video_recipe(monkeypatch, tmp_path: Path) -> None:
|
||||||
async def fake_get_default_metadata_provider():
|
async def fake_get_default_metadata_provider():
|
||||||
return SimpleNamespace(get_model_version_info=lambda id: ({}, None))
|
return SimpleNamespace(get_model_version_info=lambda id: ({}, None))
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ from types import SimpleNamespace
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from py.services.recipes.analysis_service import RecipeAnalysisService
|
from py.services.recipes.analysis_service import RecipeAnalysisService
|
||||||
from py.services.recipes.errors import RecipeDownloadError, RecipeNotFoundError
|
from py.services.recipes.errors import (
|
||||||
|
RecipeDownloadError,
|
||||||
|
RecipeNotFoundError,
|
||||||
|
RecipeValidationError,
|
||||||
|
)
|
||||||
from py.services.recipes.persistence_service import RecipePersistenceService
|
from py.services.recipes.persistence_service import RecipePersistenceService
|
||||||
|
|
||||||
|
|
||||||
@@ -486,6 +490,50 @@ async def test_move_recipe_updates_paths(tmp_path):
|
|||||||
assert stored["file_path"] == result.payload["new_file_path"]
|
assert stored["file_path"] == result.payload["new_file_path"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_recipe_accepts_gen_params() -> None:
|
||||||
|
class DummyScanner:
|
||||||
|
def __init__(self):
|
||||||
|
self.calls = []
|
||||||
|
|
||||||
|
async def update_recipe_metadata(self, recipe_id: str, updates: dict[str, object]):
|
||||||
|
self.calls.append((recipe_id, updates))
|
||||||
|
return True
|
||||||
|
|
||||||
|
scanner = DummyScanner()
|
||||||
|
service = RecipePersistenceService(
|
||||||
|
exif_utils=DummyExifUtils(),
|
||||||
|
card_preview_width=512,
|
||||||
|
logger=logging.getLogger("test"),
|
||||||
|
)
|
||||||
|
|
||||||
|
updates = {"gen_params": {"prompt": "updated prompt", "steps": 28}}
|
||||||
|
result = await service.update_recipe(
|
||||||
|
recipe_scanner=scanner,
|
||||||
|
recipe_id="recipe-1",
|
||||||
|
updates=updates,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.payload["success"] is True
|
||||||
|
assert scanner.calls == [("recipe-1", updates)]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_recipe_rejects_non_object_gen_params() -> None:
|
||||||
|
service = RecipePersistenceService(
|
||||||
|
exif_utils=DummyExifUtils(),
|
||||||
|
card_preview_width=512,
|
||||||
|
logger=logging.getLogger("test"),
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(RecipeValidationError, match="gen_params must be an object"):
|
||||||
|
await service.update_recipe(
|
||||||
|
recipe_scanner=SimpleNamespace(),
|
||||||
|
recipe_id="recipe-1",
|
||||||
|
updates={"gen_params": "invalid"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_analyze_remote_video(tmp_path):
|
async def test_analyze_remote_video(tmp_path):
|
||||||
exif_utils = DummyExifUtils()
|
exif_utils = DummyExifUtils()
|
||||||
|
|||||||
Reference in New Issue
Block a user