feat(embedding): send embedding to workflow + fix copy button format

- Fix copy button on embedding cards to copy 'embedding:folder/name' format
- Add send-embedding-to-workflow for Prompt (LoraManager), Text (LoraManager),
  and CLIPTextEncode nodes, appending embedding code to text content
- Extend workflow registry to register text-capable nodes by comfyClass
  (not generic widget name 'text') to avoid false matches
- Add mode parameter to update_node_widget API/event for append support
- Fix single/bulk context menus: single shows plain 'Send to Workflow',
  bulk collapses submenu into direct action for embeddings (append-only)
This commit is contained in:
Will Miao
2026-06-11 22:41:42 +08:00
parent 84e9fe2dfb
commit d87863b423
9 changed files with 201 additions and 20 deletions

View File

@@ -51,21 +51,33 @@ export class BulkContextMenu extends BaseContextMenu {
reimportMetadataItem.style.display = config.reimportMetadata ? 'flex' : 'none';
}
const isEmbeddings = currentModelType === 'embeddings';
if (sendToWorkflowAppendItem) {
sendToWorkflowAppendItem.style.display = config.sendToWorkflow ? 'flex' : 'none';
}
if (sendToWorkflowReplaceItem) {
sendToWorkflowReplaceItem.style.display = config.sendToWorkflow ? 'flex' : 'none';
sendToWorkflowReplaceItem.style.display = (config.sendToWorkflow && !isEmbeddings) ? 'flex' : 'none';
}
if (copyAllItem) {
copyAllItem.style.display = config.copyAll ? 'flex' : 'none';
}
// Submenu parent visibility
// Submenu parent - for embeddings, collapse into a direct item (no replace choice)
const sendToWorkflowSubmenu = this.menu.querySelector('[data-has-submenu="send-to-workflow"]');
if (sendToWorkflowSubmenu) {
const hasWorkflowActions = config.sendToWorkflow || config.copyAll;
sendToWorkflowSubmenu.style.display = hasWorkflowActions ? 'flex' : 'none';
if (isEmbeddings && config.sendToWorkflow && !config.copyAll) {
sendToWorkflowSubmenu.classList.remove('has-submenu');
sendToWorkflowSubmenu.removeAttribute('data-has-submenu');
sendToWorkflowSubmenu.dataset.action = 'send-to-workflow-append';
const arrow = sendToWorkflowSubmenu.querySelector('.submenu-arrow');
if (arrow) arrow.style.display = 'none';
const submenu = sendToWorkflowSubmenu.querySelector('.context-submenu');
if (submenu) submenu.style.display = 'none';
sendToWorkflowSubmenu.style.display = 'flex';
} else {
sendToWorkflowSubmenu.style.display = hasWorkflowActions ? 'flex' : 'none';
}
}
if (refreshAllItem) {

View File

@@ -3,6 +3,7 @@ import { ModelContextMenuMixin } from './ModelContextMenuMixin.js';
import { getModelApiClient, resetAndReload } from '../../api/modelApiFactory.js';
import { moveManager } from '../../managers/MoveManager.js';
import { showDeleteModal, showExcludeModal } from '../../utils/modalUtils.js';
import { sendEmbeddingToWorkflow } from '../../utils/uiHelpers.js';
export class EmbeddingContextMenu extends BaseContextMenu {
constructor() {
@@ -51,6 +52,13 @@ export class EmbeddingContextMenu extends BaseContextMenu {
this.currentCard.querySelector('.fa-copy').click();
}
break;
case 'sendtoworkflow': {
const folder = this.currentCard.dataset.folder || '';
const name = this.currentCard.dataset.file_name || '';
const embeddingCode = folder ? `embedding:${folder}/${name}` : `embedding:${name}`;
sendEmbeddingToWorkflow(embeddingCode, false);
break;
}
case 'refresh-metadata':
// Refresh metadata from CivitAI
apiClient.refreshSingleModelMetadata(this.currentCard.dataset.filepath);

View File

@@ -1,4 +1,4 @@
import { showToast, openCivitai, copyToClipboard, copyLoraSyntax, sendLoraToWorkflow, openExampleImagesFolder, buildLoraSyntax, sendModelPathToWorkflow } from '../../utils/uiHelpers.js';
import { showToast, openCivitai, copyToClipboard, copyLoraSyntax, sendLoraToWorkflow, sendEmbeddingToWorkflow, openExampleImagesFolder, buildLoraSyntax, sendModelPathToWorkflow } from '../../utils/uiHelpers.js';
import { state, getCurrentPageState } from '../../state/index.js';
import { showModelModal } from './ModelModal.js';
import { toggleShowcase } from './showcase/ShowcaseView.js';
@@ -216,6 +216,11 @@ function handleSendToWorkflow(card, replaceMode, modelType) {
missingNodesMessage,
missingTargetMessage,
});
} else if (modelType === MODEL_TYPES.EMBEDDING) {
const folder = card.dataset.folder || '';
const name = card.dataset.file_name || '';
const embeddingCode = folder ? `embedding:${folder}/${name}` : `embedding:${name}`;
sendEmbeddingToWorkflow(embeddingCode, false);
} else {
showToast('modelCard.sendToWorkflow.checkpointNotImplemented', {}, 'info');
}
@@ -230,8 +235,11 @@ function handleCopyAction(card, modelType) {
const message = translate('modelCard.actions.checkpointNameCopied', {}, 'Checkpoint name copied');
copyToClipboard(checkpointName, message);
} else if (modelType === MODEL_TYPES.EMBEDDING) {
const embeddingName = card.dataset.file_name;
copyToClipboard(embeddingName, 'Embedding name copied');
const folder = card.dataset.folder || '';
const name = card.dataset.file_name || '';
const embeddingCode = folder ? `embedding:${folder}/${name}` : `embedding:${name}`;
const message = translate('modelCard.actions.embeddingNameCopied', {}, 'Embedding syntax copied');
copyToClipboard(embeddingCode, message);
}
}

View File

@@ -1,4 +1,4 @@
import { showToast, openCivitai, sendLoraToWorkflow, sendModelPathToWorkflow, buildLoraSyntax } from '../../utils/uiHelpers.js';
import { showToast, openCivitai, sendLoraToWorkflow, sendEmbeddingToWorkflow, sendModelPathToWorkflow, buildLoraSyntax } from '../../utils/uiHelpers.js';
import { modalManager } from '../../managers/ModalManager.js';
import { MODEL_TYPES } from '../../api/apiConfig.js';
import {
@@ -648,6 +648,10 @@ export async function showModelModal(model, modelType) {
if (modelType === 'checkpoints' && modelWithFullData.sub_type) {
activeModalElement.dataset.subType = modelWithFullData.sub_type;
}
// Store folder for embedding models
if (modelType === 'embeddings' && modelWithFullData.folder) {
activeModalElement.dataset.folder = modelWithFullData.folder;
}
}
updateVersionsTabBadge(updateAvailabilityState.hasUpdateAvailable);
const versionsTabController = initVersionsTab({
@@ -1188,9 +1192,10 @@ async function handleSendToWorkflow(target, modelType) {
missingTargetMessage,
});
} else if (modelType === 'embeddings') {
// For Embedding: Send as LoRA syntax (embedding name only)
const embeddingSyntax = `<embed:${currentFileName}:1>`;
await sendLoraToWorkflow(embeddingSyntax, false, 'embedding');
const folder = modalElement?.dataset?.folder || '';
const name = currentFileName.replace(/\.[^.]+$/, '');
const embeddingCode = folder ? `embedding:${folder}/${name}` : `embedding:${name}`;
await sendEmbeddingToWorkflow(embeddingCode, false);
}
}