mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-06-25 12:31:15 -03:00
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 <lora:> setting before sending - Uses 'replace' mode (not append) on text-capable workflow nodes - Add translations for all 10 locales
This commit is contained in:
@@ -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(/<lora:[^>]*>/gi, '')
|
||||
.replace(/<lora:[^&]*>/gi, '')
|
||||
.replace(/,(\s*,)+/g, ',')
|
||||
.replace(/^,\s*/, '')
|
||||
.replace(/,\s*$/, '')
|
||||
.replace(/\s{2,}/g, ' ')
|
||||
.trim();
|
||||
return stripLoraTags(text);
|
||||
}
|
||||
|
||||
shouldStripLoraOnCopy() {
|
||||
|
||||
@@ -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 <lora> 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;
|
||||
|
||||
@@ -53,12 +53,19 @@ export function generateMetadataPanel(hasParams, hasPrompts, prompt, negativePro
|
||||
prompt = escapeHtml(prompt);
|
||||
content += `
|
||||
<div class="metadata-row prompt-row">
|
||||
<span class="metadata-label">Prompt:</span>
|
||||
<div class="param-header">
|
||||
<span class="metadata-label">Prompt:</span>
|
||||
<div class="param-actions">
|
||||
<button class="send-prompt-btn" data-prompt-index="${promptIndex}" title="Send Prompt to Workflow">
|
||||
<i class="fas fa-paper-plane"></i>
|
||||
</button>
|
||||
<button class="copy-prompt-btn" data-prompt-index="${promptIndex}" title="Copy Prompt">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metadata-prompt-wrapper">
|
||||
<div class="metadata-prompt">${prompt}</div>
|
||||
<button class="copy-prompt-btn" data-prompt-index="${promptIndex}">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hidden-prompt" id="prompt-${promptIndex}" style="display:none;">${prompt}</div>
|
||||
@@ -69,12 +76,19 @@ export function generateMetadataPanel(hasParams, hasPrompts, prompt, negativePro
|
||||
negativePrompt = escapeHtml(negativePrompt);
|
||||
content += `
|
||||
<div class="metadata-row prompt-row">
|
||||
<span class="metadata-label">Negative Prompt:</span>
|
||||
<div class="param-header">
|
||||
<span class="metadata-label">Negative Prompt:</span>
|
||||
<div class="param-actions">
|
||||
<button class="send-prompt-btn" data-prompt-index="${negPromptIndex}" title="Send Negative Prompt to Workflow">
|
||||
<i class="fas fa-paper-plane"></i>
|
||||
</button>
|
||||
<button class="copy-prompt-btn" data-prompt-index="${negPromptIndex}" title="Copy Negative Prompt">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metadata-prompt-wrapper">
|
||||
<div class="metadata-prompt">${negativePrompt}</div>
|
||||
<button class="copy-prompt-btn" data-prompt-index="${negPromptIndex}">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hidden-prompt" id="prompt-${negPromptIndex}" style="display:none;">${negativePrompt}</div>
|
||||
|
||||
@@ -518,6 +518,22 @@ export function copyLoraSyntax(card) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip <lora:...> tags from prompt text and clean up residual punctuation/whitespace.
|
||||
* Handles both unescaped (<lora:...>) and HTML-escaped (<lora:...>) variants.
|
||||
* Cleans up artifacts like leading ", ", double commas, and extra whitespace.
|
||||
*/
|
||||
export function stripLoraTags(text) {
|
||||
return text
|
||||
.replace(/<lora:[^>]*>/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<boolean>} 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,
|
||||
|
||||
Reference in New Issue
Block a user