From cd2628a0ee99665bf46cda2820dd8e2a90436cd6 Mon Sep 17 00:00:00 2001 From: Will Miao Date: Tue, 23 Jun 2026 21:36:24 +0800 Subject: [PATCH] feat(ui): add send-prompt-to-workflow button for prompt and negative prompt - Add sendPromptToWorkflow() and stripLoraTags() exports to uiHelpers.js - Add send button (paper-plane icon) to recipe modal and showcase hover panel - Restructure showcase metadata panel layout to match recipe modal style - Respect strip setting before sending - Uses 'replace' mode (not append) on text-capable workflow nodes - Add translations for all 10 locales --- locales/de.json | 6 +- locales/en.json | 6 +- locales/es.json | 6 +- locales/fr.json | 6 +- locales/he.json | 6 +- locales/ja.json | 6 +- locales/ko.json | 6 +- locales/ru.json | 6 +- locales/zh-CN.json | 6 +- locales/zh-TW.json | 6 +- static/css/components/lora-modal/showcase.css | 32 +++++--- static/js/components/RecipeModal.js | 45 +++++++++--- .../components/shared/showcase/MediaUtils.js | 28 ++++++- .../shared/showcase/MetadataPanel.js | 30 ++++++-- static/js/utils/uiHelpers.js | 73 +++++++++++++++++++ templates/components/recipe_modal.html | 6 ++ 16 files changed, 237 insertions(+), 37 deletions(-) diff --git a/locales/de.json b/locales/de.json index 63abc26c..b6dc7098 100644 --- a/locales/de.json +++ b/locales/de.json @@ -1620,12 +1620,15 @@ "modelUpdated": "Modell im Workflow aktualisiert", "modelFailed": "Fehler beim Aktualisieren des Modellknotens", "embeddingAdded": "Embedding zum Workflow hinzugefügt", - "embeddingFailed": "Fehler beim Hinzufügen des Embeddings" + "embeddingFailed": "Fehler beim Hinzufügen des Embeddings", + "promptSent": "Prompt an Workflow gesendet", + "promptFailed": "Fehler beim Senden des Prompts" }, "nodeSelector": { "recipe": "Rezept", "lora": "LoRA", "embedding": "Embedding", + "prompt": "Prompt", "replace": "Ersetzen", "append": "Anhängen", "selectTargetNode": "Zielknoten auswählen", @@ -1812,6 +1815,7 @@ "enterLoraName": "Bitte geben Sie einen LoRA-Namen oder Syntax ein", "reconnectedSuccessfully": "LoRA erfolgreich neu verbunden", "reconnectFailed": "Fehler beim Neuverbinden des LoRA: {message}", + "noPromptToSend": "Kein zu sendender Prompt", "cannotSend": "Kann Rezept nicht senden: Fehlende Rezept-ID", "sendFailed": "Fehler beim Senden des Rezepts an Workflow", "sendError": "Fehler beim Senden des Rezepts an Workflow", diff --git a/locales/en.json b/locales/en.json index e924fea5..f556a2a4 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1620,12 +1620,15 @@ "modelUpdated": "Model updated in workflow", "modelFailed": "Failed to update model node", "embeddingAdded": "Embedding added to workflow", - "embeddingFailed": "Failed to add embedding" + "embeddingFailed": "Failed to add embedding", + "promptSent": "Prompt sent to workflow", + "promptFailed": "Failed to send prompt" }, "nodeSelector": { "recipe": "Recipe", "lora": "LoRA", "embedding": "Embedding", + "prompt": "Prompt", "replace": "Replace", "append": "Append", "selectTargetNode": "Select target node", @@ -1812,6 +1815,7 @@ "enterLoraName": "Please enter a LoRA name or syntax", "reconnectedSuccessfully": "LoRA reconnected successfully", "reconnectFailed": "Error reconnecting LoRA: {message}", + "noPromptToSend": "No prompt to send", "cannotSend": "Cannot send recipe: Missing recipe ID", "sendFailed": "Failed to send recipe to workflow", "sendError": "Error sending recipe to workflow", diff --git a/locales/es.json b/locales/es.json index b799357f..b9e8061d 100644 --- a/locales/es.json +++ b/locales/es.json @@ -1620,12 +1620,15 @@ "modelUpdated": "Modelo actualizado en el flujo de trabajo", "modelFailed": "Error al actualizar nodo de modelo", "embeddingAdded": "Embedding añadido al flujo de trabajo", - "embeddingFailed": "Error al añadir el embedding" + "embeddingFailed": "Error al añadir el embedding", + "promptSent": "Prompt enviado al flujo de trabajo", + "promptFailed": "Error al enviar el prompt" }, "nodeSelector": { "recipe": "Receta", "lora": "LoRA", "embedding": "Embedding", + "prompt": "Prompt", "replace": "Reemplazar", "append": "Añadir", "selectTargetNode": "Seleccionar nodo de destino", @@ -1812,6 +1815,7 @@ "enterLoraName": "Por favor introduce un nombre de LoRA o sintaxis", "reconnectedSuccessfully": "LoRA reconectado exitosamente", "reconnectFailed": "Error reconectando LoRA: {message}", + "noPromptToSend": "No hay prompt para enviar", "cannotSend": "No se puede enviar receta: Falta ID de receta", "sendFailed": "Error al enviar receta al flujo de trabajo", "sendError": "Error enviando receta al flujo de trabajo", diff --git a/locales/fr.json b/locales/fr.json index 73b07221..eed801dc 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -1620,12 +1620,15 @@ "modelUpdated": "Modèle mis à jour dans le workflow", "modelFailed": "Échec de la mise à jour du nœud modèle", "embeddingAdded": "Embedding ajouté au workflow", - "embeddingFailed": "Échec de l'ajout de l'embedding" + "embeddingFailed": "Échec de l'ajout de l'embedding", + "promptSent": "Prompt envoyé au workflow", + "promptFailed": "Échec de l'envoi du prompt" }, "nodeSelector": { "recipe": "Recipe", "lora": "LoRA", "embedding": "Embedding", + "prompt": "Prompt", "replace": "Remplacer", "append": "Ajouter", "selectTargetNode": "Sélectionner le nœud cible", @@ -1812,6 +1815,7 @@ "enterLoraName": "Veuillez entrer un nom ou une syntaxe LoRA", "reconnectedSuccessfully": "LoRA reconnecté avec succès", "reconnectFailed": "Erreur lors de la reconnexion du LoRA : {message}", + "noPromptToSend": "Aucun prompt à envoyer", "cannotSend": "Impossible d'envoyer la recipe : ID de recipe manquant", "sendFailed": "Échec de l'envoi de la recipe vers le workflow", "sendError": "Erreur lors de l'envoi de la recipe vers le workflow", diff --git a/locales/he.json b/locales/he.json index 23020f5a..877a86f4 100644 --- a/locales/he.json +++ b/locales/he.json @@ -1620,12 +1620,15 @@ "modelUpdated": "מודל עודכן ב-workflow", "modelFailed": "עדכון צומת המודל נכשל", "embeddingAdded": "Embedding נוסף ל-workflow", - "embeddingFailed": "הוספת Embedding נכשלה" + "embeddingFailed": "הוספת Embedding נכשלה", + "promptSent": "הנחיה נשלחה ל-workflow", + "promptFailed": "שליחת ההנחיה נכשלה" }, "nodeSelector": { "recipe": "מתכון", "lora": "LoRA", "embedding": "Embedding", + "prompt": "הנחיה", "replace": "החלף", "append": "הוסף", "selectTargetNode": "בחר צומת יעד", @@ -1812,6 +1815,7 @@ "enterLoraName": "אנא הזן שם LoRA או תחביר", "reconnectedSuccessfully": "LoRA קושר מחדש בהצלחה", "reconnectFailed": "שגיאה בקישור מחדש של LoRA: {message}", + "noPromptToSend": "אין הנחיה לשליחה", "cannotSend": "לא ניתן לשלוח מתכון: חסר מזהה מתכון", "sendFailed": "שליחת המתכון ל-workflow נכשלה", "sendError": "שגיאה בשליחת המתכון ל-workflow", diff --git a/locales/ja.json b/locales/ja.json index 0e20e311..dc8eb83e 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -1620,12 +1620,15 @@ "modelUpdated": "モデルがワークフローで更新されました", "modelFailed": "モデルノードの更新に失敗しました", "embeddingAdded": "Embeddingをワークフローに追加しました", - "embeddingFailed": "Embeddingの追加に失敗しました" + "embeddingFailed": "Embeddingの追加に失敗しました", + "promptSent": "プロンプトをワークフローに送信しました", + "promptFailed": "プロンプトの送信に失敗しました" }, "nodeSelector": { "recipe": "レシピ", "lora": "LoRA", "embedding": "Embedding", + "prompt": "プロンプト", "replace": "置換", "append": "追加", "selectTargetNode": "ターゲットノードを選択", @@ -1812,6 +1815,7 @@ "enterLoraName": "LoRA名または構文を入力してください", "reconnectedSuccessfully": "LoRAが正常に再接続されました", "reconnectFailed": "LoRA再接続エラー:{message}", + "noPromptToSend": "送信するプロンプトがありません", "cannotSend": "レシピを送信できません:レシピIDがありません", "sendFailed": "レシピのワークフローへの送信に失敗しました", "sendError": "レシピのワークフロー送信エラー", diff --git a/locales/ko.json b/locales/ko.json index 9fd5773b..76a78cb4 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -1620,12 +1620,15 @@ "modelUpdated": "모델이 워크플로에서 업데이트되었습니다", "modelFailed": "모델 노드 업데이트 실패", "embeddingAdded": "Embedding을 워크플로에 추가했습니다", - "embeddingFailed": "Embedding 추가 실패" + "embeddingFailed": "Embedding 추가 실패", + "promptSent": "프롬프트를 워크플로에 보냈습니다", + "promptFailed": "프롬프트 보내기 실패" }, "nodeSelector": { "recipe": "레시피", "lora": "LoRA", "embedding": "임베딩", + "prompt": "프롬프트", "replace": "교체", "append": "추가", "selectTargetNode": "대상 노드 선택", @@ -1812,6 +1815,7 @@ "enterLoraName": "LoRA 이름 또는 문법을 입력해주세요", "reconnectedSuccessfully": "LoRA가 성공적으로 다시 연결되었습니다", "reconnectFailed": "LoRA 다시 연결 오류: {message}", + "noPromptToSend": "보낼 프롬프트가 없습니다", "cannotSend": "레시피를 전송할 수 없습니다: 레시피 ID 누락", "sendFailed": "레시피를 워크플로로 전송하는데 실패했습니다", "sendError": "레시피를 워크플로로 전송하는 중 오류", diff --git a/locales/ru.json b/locales/ru.json index d52b0625..30061fc4 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -1620,12 +1620,15 @@ "modelUpdated": "Модель обновлена в workflow", "modelFailed": "Не удалось обновить узел модели", "embeddingAdded": "Embedding добавлен в workflow", - "embeddingFailed": "Не удалось добавить embedding" + "embeddingFailed": "Не удалось добавить embedding", + "promptSent": "Запрос отправлен в workflow", + "promptFailed": "Не удалось отправить запрос" }, "nodeSelector": { "recipe": "Рецепт", "lora": "LoRA", "embedding": "Эмбеддинг", + "prompt": "Запрос", "replace": "Заменить", "append": "Добавить", "selectTargetNode": "Выберите целевой узел", @@ -1812,6 +1815,7 @@ "enterLoraName": "Пожалуйста, введите название LoRA или синтаксис", "reconnectedSuccessfully": "LoRA успешно переподключена", "reconnectFailed": "Ошибка переподключения LoRA: {message}", + "noPromptToSend": "Нет запроса для отправки", "cannotSend": "Невозможно отправить рецепт: отсутствует ID рецепта", "sendFailed": "Не удалось отправить рецепт в workflow", "sendError": "Ошибка отправки рецепта в workflow", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 94f6e56a..1ecc3ad6 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -1620,12 +1620,15 @@ "modelUpdated": "模型已更新到工作流", "modelFailed": "更新模型节点失败", "embeddingAdded": "Embedding 已追加到工作流", - "embeddingFailed": "添加 Embedding 失败" + "embeddingFailed": "添加 Embedding 失败", + "promptSent": "提示词已发送到工作流", + "promptFailed": "提示词发送失败" }, "nodeSelector": { "recipe": "配方", "lora": "LoRA", "embedding": "Embedding", + "prompt": "提示词", "replace": "替换", "append": "追加", "selectTargetNode": "选择目标节点", @@ -1812,6 +1815,7 @@ "enterLoraName": "请输入 LoRA 名称或语法", "reconnectedSuccessfully": "LoRA 重新连接成功", "reconnectFailed": "LoRA 重新连接出错:{message}", + "noPromptToSend": "没有可发送的提示词", "cannotSend": "无法发送配方:缺少配方 ID", "sendFailed": "发送配方到工作流失败", "sendError": "发送配方到工作流出错", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index a164da52..b321fffb 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -1620,12 +1620,15 @@ "modelUpdated": "模型已更新到工作流", "modelFailed": "更新模型節點失敗", "embeddingAdded": "Embedding 已附加到工作流", - "embeddingFailed": "傳送 Embedding 到工作流失敗" + "embeddingFailed": "傳送 Embedding 到工作流失敗", + "promptSent": "提示詞已發送到工作流", + "promptFailed": "提示詞發送失敗" }, "nodeSelector": { "recipe": "配方", "lora": "LoRA", "embedding": "Embedding", + "prompt": "提示詞", "replace": "取代", "append": "附加", "selectTargetNode": "選擇目標節點", @@ -1812,6 +1815,7 @@ "enterLoraName": "請輸入 LoRA 名稱或語法", "reconnectedSuccessfully": "LoRA 重新連結成功", "reconnectFailed": "LoRA 重新連結錯誤:{message}", + "noPromptToSend": "沒有可發送的提示詞", "cannotSend": "無法傳送配方:缺少配方 ID", "sendFailed": "傳送配方到工作流失敗", "sendError": "傳送配方到工作流錯誤", diff --git a/static/css/components/lora-modal/showcase.css b/static/css/components/lora-modal/showcase.css index 77c6ae3a..63373638 100644 --- a/static/css/components/lora-modal/showcase.css +++ b/static/css/components/lora-modal/showcase.css @@ -272,13 +272,25 @@ margin-top: var(--space-2); } +.metadata-row.prompt-row .param-header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; + margin-bottom: 4px; +} + +.metadata-row.prompt-row .param-actions { + display: flex; + align-items: center; + gap: 4px; +} + .metadata-label { font-weight: 600; color: var(--text-color); opacity: 0.8; font-size: 0.85em; - display: block; - margin-bottom: 4px; } .metadata-prompt-wrapper { @@ -286,7 +298,7 @@ background: var(--lora-surface); border: 1px solid var(--lora-border); border-radius: var(--border-radius-xs); - padding: 6px 30px 6px 8px; + padding: 6px 8px; margin-top: 2px; max-height: 80px; /* Reduced from 120px */ overflow-y: auto; @@ -302,22 +314,24 @@ white-space: pre-wrap; } -.copy-prompt-btn { - position: absolute; - top: 6px; - right: 6px; +.copy-prompt-btn, +.send-prompt-btn { background: transparent; border: none; color: var(--text-color); opacity: 0.6; cursor: pointer; - padding: 3px; + padding: 3px 6px; + border-radius: var(--border-radius-xs); transition: var(--transition-base); + font-size: 0.9em; } -.copy-prompt-btn:hover { +.copy-prompt-btn:hover, +.send-prompt-btn:hover { opacity: 1; color: var(--lora-accent); + background: var(--lora-surface); } /* Scrollbar styling for metadata panel */ diff --git a/static/js/components/RecipeModal.js b/static/js/components/RecipeModal.js index e3598326..68d6298e 100644 --- a/static/js/components/RecipeModal.js +++ b/static/js/components/RecipeModal.js @@ -1,5 +1,5 @@ // Recipe Modal Component -import { showToast, copyToClipboard, sendLoraToWorkflow, sendModelPathToWorkflow, openCivitaiByMetadata } from '../utils/uiHelpers.js'; +import { showToast, copyToClipboard, sendLoraToWorkflow, sendModelPathToWorkflow, openCivitaiByMetadata, stripLoraTags, sendPromptToWorkflow } from '../utils/uiHelpers.js'; import { translate } from '../utils/i18nHelpers.js'; import { state } from '../state/index.js'; import { setSessionItem, removeSessionItem, getStorageItem, setStorageItem } from '../utils/storageHelpers.js'; @@ -1200,6 +1200,40 @@ class RecipeModal { this.sendRecipeToWorkflow(); }); } + + // Send prompt to workflow buttons + const sendPromptBtn = document.getElementById('sendPromptBtn'); + const sendNegativePromptBtn = document.getElementById('sendNegativePromptBtn'); + + if (sendPromptBtn) { + sendPromptBtn.addEventListener('click', () => { + let promptText = this.currentRecipe?.gen_params?.prompt || ''; + if (this.shouldStripLoraOnCopy()) { + promptText = RecipeModal.stripLoraTags(promptText); + } + if (!promptText.trim()) { + showToast('toast.recipes.noPromptToSend', {}, 'warning'); + return; + } + sendPromptToWorkflow(promptText); + }); + } + + if (sendNegativePromptBtn) { + sendNegativePromptBtn.addEventListener('click', () => { + let negativePromptText = this.currentRecipe?.gen_params?.negative_prompt || ''; + if (this.shouldStripLoraOnCopy()) { + negativePromptText = RecipeModal.stripLoraTags(negativePromptText); + } + if (!negativePromptText.trim()) { + showToast('toast.recipes.noPromptToSend', {}, 'warning'); + return; + } + sendPromptToWorkflow(negativePromptText, { + actionTypeText: 'Negative Prompt', + }); + }); + } } /** @@ -1208,14 +1242,7 @@ class RecipeModal { * Cleans up artifacts like leading ", ", double commas, and extra whitespace. */ static stripLoraTags(text) { - return text - .replace(/]*>/gi, '') - .replace(/<lora:[^&]*>/gi, '') - .replace(/,(\s*,)+/g, ',') - .replace(/^,\s*/, '') - .replace(/,\s*$/, '') - .replace(/\s{2,}/g, ' ') - .trim(); + return stripLoraTags(text); } shouldStripLoraOnCopy() { diff --git a/static/js/components/shared/showcase/MediaUtils.js b/static/js/components/shared/showcase/MediaUtils.js index 9660ee62..d3282726 100644 --- a/static/js/components/shared/showcase/MediaUtils.js +++ b/static/js/components/shared/showcase/MediaUtils.js @@ -3,7 +3,7 @@ * Media-specific utility functions for showcase components * (Moved from uiHelpers.js to better organize code) */ -import { showToast, copyToClipboard, getNSFWLevelName } from '../../../utils/uiHelpers.js'; +import { showToast, copyToClipboard, getNSFWLevelName, sendPromptToWorkflow, stripLoraTags } from '../../../utils/uiHelpers.js'; import { state } from '../../../state/index.js'; import { getModelApiClient } from '../../../api/modelApiFactory.js'; import { NSFW_LEVELS, getMatureBlurThreshold } from '../../../utils/constants.js'; @@ -318,6 +318,32 @@ export function initMetadataPanelHandlers(container) { }); }); + // Handle send prompt buttons + const sendBtns = metadataPanel.querySelectorAll('.send-prompt-btn'); + sendBtns.forEach(sendBtn => { + const promptIndex = sendBtn.dataset.promptIndex; + const promptElement = wrapper.querySelector(`#prompt-${promptIndex}`); + + sendBtn.addEventListener('click', async (e) => { + e.stopPropagation(); + + if (!promptElement) return; + + let promptText = promptElement.textContent || ''; + if (!promptText.trim()) { + showToast('toast.recipes.noPromptToSend', {}, 'warning'); + return; + } + + // Respect strip setting from global state + if (state.global.settings?.strip_lora_on_copy) { + promptText = stripLoraTags(promptText); + } + + sendPromptToWorkflow(promptText); + }); + }); + // Prevent panel scroll from causing modal scroll metadataPanel.addEventListener('wheel', (e) => { const isAtTop = metadataPanel.scrollTop === 0; diff --git a/static/js/components/shared/showcase/MetadataPanel.js b/static/js/components/shared/showcase/MetadataPanel.js index f968348a..053e2f7d 100644 --- a/static/js/components/shared/showcase/MetadataPanel.js +++ b/static/js/components/shared/showcase/MetadataPanel.js @@ -53,12 +53,19 @@ export function generateMetadataPanel(hasParams, hasPrompts, prompt, negativePro prompt = escapeHtml(prompt); content += ` @@ -69,12 +76,19 @@ export function generateMetadataPanel(hasParams, hasPrompts, prompt, negativePro negativePrompt = escapeHtml(negativePrompt); content += ` diff --git a/static/js/utils/uiHelpers.js b/static/js/utils/uiHelpers.js index afe5b9fe..a0184d70 100644 --- a/static/js/utils/uiHelpers.js +++ b/static/js/utils/uiHelpers.js @@ -518,6 +518,22 @@ export function copyLoraSyntax(card) { } } +/** + * Strip tags from prompt text and clean up residual punctuation/whitespace. + * Handles both unescaped () and HTML-escaped (<lora:...>) variants. + * Cleans up artifacts like leading ", ", double commas, and extra whitespace. + */ +export function stripLoraTags(text) { + return text + .replace(/]*>/gi, '') + .replace(/<lora:[^&]*>/gi, '') + .replace(/,(\s*,)+/g, ',') + .replace(/^,\s*/, '') + .replace(/,\s*$/, '') + .replace(/\s{2,}/g, ' ') + .trim(); +} + async function fetchWorkflowRegistry() { try { const response = await fetch('/api/lm/get-registry'); @@ -983,6 +999,63 @@ export async function sendEmbeddingToWorkflow(embeddingCode) { return true; } +/** + * Send prompt text to workflow text-capable nodes (replaces existing content). + * Uses the same target node discovery as sendEmbeddingToWorkflow. + * @param {string} promptText - The prompt/negative prompt text to send + * @param {Object} [options] - Optional messages overrides + * @param {string} [options.actionTypeText] - Label for the action type (default "Prompt") + * @param {string} [options.successMessage] - Success toast message + * @param {string} [options.failureMessage] - Failure toast message + * @param {string} [options.missingNodesMessage] - No nodes warning message + * @param {string} [options.missingTargetMessage] - No target selected warning message + * @returns {Promise} Whether the send succeeded + */ +export async function sendPromptToWorkflow(promptText, options = {}) { + const registry = await fetchWorkflowRegistry(); + if (!registry) { + return false; + } + + const textNodes = filterRegistryNodes(registry.nodes, (node) => { + if (!isNodeEnabled(node)) { + return false; + } + return ( + node.capabilities?.has_text_widget === true || + node.marker_role === "send_prompt_target" + ); + }); + + const nodeKeys = Object.keys(textNodes); + if (nodeKeys.length === 0) { + showToast(options.missingNodesMessage || 'uiHelpers.workflow.noMatchingNodes', {}, 'warning'); + return false; + } + + const messages = { + successMessage: options.successMessage || translate('uiHelpers.workflow.promptSent', {}, 'Prompt sent to workflow'), + failureMessage: options.failureMessage || translate('uiHelpers.workflow.promptFailed', {}, 'Failed to send prompt'), + missingTargetMessage: options.missingTargetMessage || translate('uiHelpers.workflow.noTargetNodeSelected', {}, 'No target node selected'), + }; + + const handleSend = (selectedNodeIds) => + sendTextToNodes(selectedNodeIds, textNodes, promptText, 'replace', messages); + + if (nodeKeys.length === 1) { + return await handleSend([nodeKeys[0]]); + } + + const actionType = options.actionTypeText || translate('uiHelpers.nodeSelector.prompt', {}, 'Prompt'); + + showNodeSelector(textNodes, { + actionType, + actionMode: translate('uiHelpers.nodeSelector.replace', {}, 'Replace'), + onSend: handleSend, + }); + return true; +} + // Global variable to track active node selector state let nodeSelectorState = { isActive: false, diff --git a/templates/components/recipe_modal.html b/templates/components/recipe_modal.html index a9271bac..7624e170 100644 --- a/templates/components/recipe_modal.html +++ b/templates/components/recipe_modal.html @@ -36,6 +36,9 @@
+ @@ -62,6 +65,9 @@
+