feat: add 'Send to ComfyUI' button to ModelModal and RecipeModal

- Add send button to ModelModal header for all model types (LoRA, Checkpoint, Embedding)
- Add send button to RecipeModal header for sending entire recipes
- Style buttons to match existing modal action buttons
- Add translations for all supported languages
This commit is contained in:
Will Miao
2026-03-29 20:35:08 +08:00
parent a4cb51e96c
commit 267082c712
15 changed files with 262 additions and 14 deletions

View File

@@ -835,7 +835,8 @@
}
[data-theme="dark"] .creator-info,
[data-theme="dark"] .civitai-view {
[data-theme="dark"] .civitai-view,
[data-theme="dark"] .modal-send-btn {
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--lora-border);
}
@@ -875,7 +876,8 @@
/* Add hover effect for creator info */
.creator-info:hover,
.civitai-view:hover {
.civitai-view:hover,
.modal-send-btn:hover {
background: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.1);
border-color: var(--lora-accent);
transform: translateY(-1px);
@@ -910,3 +912,42 @@
align-items: center;
justify-content: center;
}
/* Send to ComfyUI Button */
.modal-send-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: rgba(0, 0, 0, 0.03);
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: var(--border-radius-sm);
color: var(--text-color);
cursor: pointer;
font-weight: 500;
font-size: 0.9em;
transition: all 0.2s;
}
[data-theme="dark"] .modal-send-btn {
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--lora-border);
}
.modal-send-btn:hover {
background: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.1);
border-color: var(--lora-accent);
transform: translateY(-1px);
}
.modal-send-btn:active {
transform: translateY(0);
}
.modal-send-btn i {
font-size: 14px;
}
.modal-send-btn span {
white-space: nowrap;
}

View File

@@ -565,6 +565,26 @@
color: var(--lora-accent);
}
.send-recipe-btn {
background: none;
border: none;
color: var(--text-color);
opacity: 0.7;
cursor: pointer;
padding: 4px 8px;
border-radius: var(--border-radius-xs);
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.send-recipe-btn:hover {
opacity: 1;
background: var(--lora-surface);
color: var(--lora-accent);
}
#recipeLorasCount {
font-size: 0.9em;
color: var(--text-color);

View File

@@ -1,5 +1,5 @@
// Recipe Modal Component
import { showToast, copyToClipboard, sendModelPathToWorkflow, openCivitaiByMetadata } from '../utils/uiHelpers.js';
import { showToast, copyToClipboard, sendLoraToWorkflow, sendModelPathToWorkflow, openCivitaiByMetadata } from '../utils/uiHelpers.js';
import { translate } from '../utils/i18nHelpers.js';
import { state } from '../state/index.js';
import { setSessionItem, removeSessionItem } from '../utils/storageHelpers.js';
@@ -778,6 +778,7 @@ class RecipeModal {
const copyPromptBtn = document.getElementById('copyPromptBtn');
const copyNegativePromptBtn = document.getElementById('copyNegativePromptBtn');
const copyRecipeSyntaxBtn = document.getElementById('copyRecipeSyntaxBtn');
const sendRecipeBtn = document.getElementById('sendRecipeBtn');
if (copyPromptBtn) {
copyPromptBtn.addEventListener('click', () => {
@@ -799,6 +800,13 @@ class RecipeModal {
this.fetchAndCopyRecipeSyntax();
});
}
if (sendRecipeBtn) {
sendRecipeBtn.addEventListener('click', () => {
// Send recipe to ComfyUI workflow
this.sendRecipeToWorkflow();
});
}
}
// Fetch recipe syntax from backend and copy to clipboard
@@ -835,6 +843,35 @@ class RecipeModal {
copyToClipboard(text, successMessage);
}
// Send recipe to ComfyUI workflow
async sendRecipeToWorkflow() {
if (!this.recipeId) {
showToast('toast.recipes.noRecipeId', {}, 'error');
return;
}
try {
// Fetch recipe syntax from backend
const response = await fetch(`/api/lm/recipe/${this.recipeId}/syntax`);
if (!response.ok) {
throw new Error(`Failed to get recipe syntax: ${response.statusText}`);
}
const data = await response.json();
if (data.success && data.syntax) {
// Send the recipe syntax to ComfyUI workflow
await sendLoraToWorkflow(data.syntax, false, 'recipe');
} else {
throw new Error(data.error || 'No syntax returned from server');
}
} catch (error) {
console.error('Error sending recipe to workflow:', error);
showToast('toast.recipes.sendToWorkflowFailed', { message: error.message }, 'error');
}
}
// Add new method to handle downloading missing LoRAs
async showDownloadMissingLorasModal() {
console.log("currentRecipe", this.currentRecipe);

View File

@@ -1,5 +1,6 @@
import { showToast, openCivitai } from '../../utils/uiHelpers.js';
import { showToast, openCivitai, sendLoraToWorkflow, sendModelPathToWorkflow, buildLoraSyntax } from '../../utils/uiHelpers.js';
import { modalManager } from '../../managers/ModalManager.js';
import { MODEL_TYPES } from '../../api/apiConfig.js';
import {
toggleShowcase,
setupShowcaseScroll,
@@ -294,6 +295,17 @@ export async function showModelModal(model, modelType) {
].join('\n')
: '';
const headerActionItems = [];
// Add send to ComfyUI button for all model types
const sendToWorkflowTitle = translate('modals.model.actions.sendToWorkflow', {}, 'Send to ComfyUI');
const sendToWorkflowButton = `
<button class="modal-send-btn" data-action="send-to-workflow" data-model-type="${modelType}" title="${sendToWorkflowTitle}">
<i class="fas fa-paper-plane"></i>
<span>${translate('modals.model.actions.sendToWorkflowText', {}, 'Send to ComfyUI')}</span>
</button>
`.trim();
headerActionItems.push(indentMarkup(sendToWorkflowButton, 20));
if (creatorActionsMarkup) {
headerActionItems.push(creatorActionsMarkup);
}
@@ -615,6 +627,14 @@ export async function showModelModal(model, modelType) {
const activeModalElement = document.getElementById(modalId);
if (activeModalElement) {
activeModalElement.dataset.filePath = modelWithFullData.file_path || '';
// Store usage_tips for LoRA models
if (modelType === 'loras' && modelWithFullData.usage_tips) {
activeModalElement.dataset.usageTips = modelWithFullData.usage_tips;
}
// Store sub_type for checkpoint models
if (modelType === 'checkpoints' && modelWithFullData.sub_type) {
activeModalElement.dataset.subType = modelWithFullData.sub_type;
}
}
updateVersionsTabBadge(updateAvailabilityState.hasUpdateAvailable);
const versionsTabController = initVersionsTab({
@@ -747,6 +767,9 @@ function setupEventHandlers(filePath, modelType) {
case 'nav-next':
handleDirectionalNavigation('next', modelType);
break;
case 'send-to-workflow':
handleSendToWorkflow(target, modelType);
break;
}
}
@@ -1026,6 +1049,70 @@ async function openFileLocation(filePath) {
}
}
async function handleSendToWorkflow(target, modelType) {
const filePath = getModalFilePath();
if (!filePath) {
showToast('modals.model.sendToWorkflow.noFilePath', {}, 'error');
return;
}
// Get the current model data from the modal
const modalElement = document.getElementById('modelModal');
const currentFileName = modalElement?.querySelector('#file-name')?.textContent || '';
if (modelType === 'loras') {
// For LoRA: Build syntax from usage tips and send
const usageTipsData = modalElement?.dataset?.usageTips;
const usageTips = usageTipsData ? JSON.parse(usageTipsData) : {};
const loraSyntax = buildLoraSyntax(currentFileName, usageTips);
await sendLoraToWorkflow(loraSyntax, false, 'lora');
} else if (modelType === 'checkpoints') {
// For Checkpoint: Send model path
const subtype = (modalElement?.dataset?.subType || 'checkpoint').toLowerCase();
const isDiffusionModel = subtype === 'diffusion_model';
const widgetName = isDiffusionModel ? 'unet_name' : 'ckpt_name';
const actionTypeText = translate(
isDiffusionModel ? 'uiHelpers.nodeSelector.diffusionModel' : 'uiHelpers.nodeSelector.checkpoint',
{},
isDiffusionModel ? 'Diffusion Model' : 'Checkpoint'
);
const successMessage = translate(
isDiffusionModel ? 'uiHelpers.workflow.diffusionModelUpdated' : 'uiHelpers.workflow.checkpointUpdated',
{},
isDiffusionModel ? 'Diffusion model updated in workflow' : 'Checkpoint updated in workflow'
);
const failureMessage = translate(
isDiffusionModel ? 'uiHelpers.workflow.diffusionModelFailed' : 'uiHelpers.workflow.checkpointFailed',
{},
isDiffusionModel ? 'Failed to update diffusion model node' : 'Failed to update checkpoint node'
);
const missingNodesMessage = translate(
'uiHelpers.workflow.noMatchingNodes',
{},
'No compatible nodes available in the current workflow'
);
const missingTargetMessage = translate(
'uiHelpers.workflow.noTargetNodeSelected',
{},
'No target node selected'
);
await sendModelPathToWorkflow(filePath, {
widgetName,
collectionType: MODEL_TYPES.CHECKPOINT,
actionTypeText,
successMessage,
failureMessage,
missingNodesMessage,
missingTargetMessage,
});
} else if (modelType === 'embeddings') {
// For Embedding: Send as LoRA syntax (embedding name only)
const embeddingSyntax = `<embed:${currentFileName}:1>`;
await sendLoraToWorkflow(embeddingSyntax, false, 'embedding');
}
}
// Export the model modal API
const modelModal = {
show: showModelModal,