mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-06-11 13:19:24 -03:00
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:
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { state, getCurrentPageState } from '../state/index.js';
|
||||
import { showToast, copyToClipboard, sendLoraToWorkflow, buildLoraSyntax, getNSFWLevelName } from '../utils/uiHelpers.js';
|
||||
import { showToast, copyToClipboard, sendLoraToWorkflow, sendEmbeddingToWorkflow, buildLoraSyntax, getNSFWLevelName } from '../utils/uiHelpers.js';
|
||||
import { updateCardsForBulkMode } from '../components/shared/ModelCard.js';
|
||||
import { modalManager } from './ModalManager.js';
|
||||
import { getModelApiClient, resetAndReload } from '../api/modelApiFactory.js';
|
||||
@@ -47,7 +47,7 @@ export class BulkManager {
|
||||
},
|
||||
[MODEL_TYPES.EMBEDDING]: {
|
||||
addTags: true,
|
||||
sendToWorkflow: false,
|
||||
sendToWorkflow: true,
|
||||
copyAll: false,
|
||||
refreshAll: true,
|
||||
checkUpdates: true,
|
||||
@@ -504,13 +504,17 @@ export class BulkManager {
|
||||
}
|
||||
|
||||
async sendAllModelsToWorkflow(replaceMode = false) {
|
||||
if (state.currentPageType !== MODEL_TYPES.LORA) {
|
||||
showToast('toast.loras.sendOnlyForLoras', {}, 'warning');
|
||||
if (state.selectedModels.size === 0) {
|
||||
showToast('toast.models.noModelsSelected', {}, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.selectedModels.size === 0) {
|
||||
showToast('toast.loras.noLorasSelected', {}, 'warning');
|
||||
if (state.currentPageType === MODEL_TYPES.EMBEDDING) {
|
||||
return this._sendAllEmbeddingsToWorkflow();
|
||||
}
|
||||
|
||||
if (state.currentPageType !== MODEL_TYPES.LORA) {
|
||||
showToast('toast.loras.sendOnlyForLoras', {}, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -542,6 +546,28 @@ export class BulkManager {
|
||||
await sendLoraToWorkflow(loraSyntaxes.join(', '), replaceMode, 'lora');
|
||||
}
|
||||
|
||||
async _sendAllEmbeddingsToWorkflow() {
|
||||
const embeddingCodes = [];
|
||||
for (const filepath of state.selectedModels) {
|
||||
const escapedPath = CSS.escape(filepath);
|
||||
const card = document.querySelector(`.model-card[data-filepath="${escapedPath}"]`);
|
||||
if (card) {
|
||||
const folder = card.dataset.folder || '';
|
||||
const name = card.dataset.file_name || '';
|
||||
const code = folder ? `embedding:${folder}/${name}` : `embedding:${name}`;
|
||||
embeddingCodes.push(code);
|
||||
}
|
||||
}
|
||||
|
||||
if (embeddingCodes.length === 0) {
|
||||
showToast('No valid embedding data found', {}, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const joinedCode = embeddingCodes.join(', ');
|
||||
await sendEmbeddingToWorkflow(joinedCode, false);
|
||||
}
|
||||
|
||||
showBulkDeleteModal() {
|
||||
if (state.selectedModels.size === 0) {
|
||||
showToast('toast.models.noModelsSelected', {}, 'warning');
|
||||
|
||||
@@ -866,6 +866,108 @@ async function sendWidgetValueToNodes(nodeIds, nodesMap, widgetName, value, mess
|
||||
}
|
||||
}
|
||||
|
||||
async function sendTextToNodes(nodeIds, nodesMap, text, mode, messages = {}) {
|
||||
const {
|
||||
successMessage = 'Updated workflow node',
|
||||
failureMessage = 'Failed to update workflow node',
|
||||
missingTargetMessage = 'No target node selected',
|
||||
} = messages;
|
||||
|
||||
const targetIds = Array.isArray(nodeIds) ? nodeIds : [];
|
||||
if (targetIds.length === 0) {
|
||||
showToast(missingTargetMessage, {}, 'warning');
|
||||
return false;
|
||||
}
|
||||
|
||||
const references = targetIds
|
||||
.map((nodeKey) => resolveNodeReference(nodeKey, nodesMap))
|
||||
.filter((reference) => reference && reference.node_id !== undefined);
|
||||
|
||||
if (references.length === 0) {
|
||||
showToast(missingTargetMessage, {}, 'warning');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/lm/update-node-widget', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
widget_name: 'text',
|
||||
value: text,
|
||||
mode: mode || 'append',
|
||||
node_ids: references,
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
showToast(successMessage, {}, 'success');
|
||||
return true;
|
||||
}
|
||||
|
||||
const errorMessage = result?.error || failureMessage;
|
||||
showToast(errorMessage, {}, 'error');
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('Failed to send text to workflow:', error);
|
||||
showToast(failureMessage, {}, 'error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendEmbeddingToWorkflow(embeddingCode, replaceMode = false) {
|
||||
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;
|
||||
});
|
||||
|
||||
const nodeKeys = Object.keys(textNodes);
|
||||
if (nodeKeys.length === 0) {
|
||||
showToast('uiHelpers.workflow.noMatchingNodes', {}, 'warning');
|
||||
return false;
|
||||
}
|
||||
|
||||
const mode = replaceMode ? 'replace' : 'append';
|
||||
const messages = {
|
||||
successMessage: translate(
|
||||
replaceMode ? 'uiHelpers.workflow.embeddingReplaced' : 'uiHelpers.workflow.embeddingAdded',
|
||||
{},
|
||||
replaceMode ? 'Embedding replaced in workflow' : 'Embedding added to workflow'
|
||||
),
|
||||
failureMessage: translate('uiHelpers.workflow.embeddingFailed', {}, 'Failed to add embedding'),
|
||||
missingTargetMessage: translate('uiHelpers.workflow.noTargetNodeSelected', {}, 'No target node selected'),
|
||||
};
|
||||
|
||||
const handleSend = (selectedNodeIds) =>
|
||||
sendTextToNodes(selectedNodeIds, textNodes, embeddingCode, mode, messages);
|
||||
|
||||
if (nodeKeys.length === 1) {
|
||||
return await handleSend([nodeKeys[0]]);
|
||||
}
|
||||
|
||||
const actionType = translate('uiHelpers.nodeSelector.embedding', {}, 'Embedding');
|
||||
const actionMode = replaceMode
|
||||
? translate('uiHelpers.nodeSelector.replace', {}, 'Replace')
|
||||
: translate('uiHelpers.nodeSelector.append', {}, 'Append');
|
||||
|
||||
showNodeSelector(textNodes, {
|
||||
actionType,
|
||||
actionMode,
|
||||
onSend: handleSend,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// Global variable to track active node selector state
|
||||
let nodeSelectorState = {
|
||||
isActive: false,
|
||||
|
||||
Reference in New Issue
Block a user