Files
ComfyUI-Lora-Manager/static/js/components/ContextMenu/LoraContextMenu.js
Will Miao f86198d234 fix(loras): include folder prefix in context menu and bulk send-to-workflow
When using full path lora syntax, the context menu (single/bulk)
and bulk copy actions were passing only the file basename to
buildLoraSyntax(), ignoring the folder prefix. This caused the
output to look like legacy A1111 format even when full path mode
was enabled.

Aligns all entry points with ModelCard.handleSendToWorkflow(),
which correctly includes the folder prefix.

Also fixes selectAllVisibleModels() to cache the folder field,
preventing missing prefix on select-all-then-send flows.
2026-07-16 18:54:00 +08:00

165 lines
6.5 KiB
JavaScript

import { BaseContextMenu } from './BaseContextMenu.js';
import { ModelContextMenuMixin } from './ModelContextMenuMixin.js';
import { state } from '../../state/index.js';
import { getModelApiClient, resetAndReload } from '../../api/modelApiFactory.js';
import { copyLoraSyntax, sendLoraToWorkflow, buildLoraSyntax, showToast } from '../../utils/uiHelpers.js';
import { showExcludeModal, showDeleteModal } from '../../utils/modalUtils.js';
import { moveManager } from '../../managers/MoveManager.js';
export class LoraContextMenu extends BaseContextMenu {
constructor() {
super('loraContextMenu', '.model-card');
this.nsfwSelector = document.getElementById('nsfwLevelSelector');
this.modelType = 'lora';
this.resetAndReload = resetAndReload;
this.initNSFWSelector();
}
// Use the saveModelMetadata implementation from loraApi
async saveModelMetadata(filePath, data) {
return getModelApiClient().saveModelMetadata(filePath, data);
}
showMenu(x, y, card) {
super.showMenu(x, y, card);
this.updateExcludeMenuItem();
this.updateEnrichMenuItem(card);
}
updateEnrichMenuItem(card) {
const enrichItem = this.menu?.querySelector('[data-action="enrich-hf-llm"]');
if (!enrichItem) return;
const hasHfUrl = !!card.dataset.hf_url;
enrichItem.classList.toggle('disabled', !hasHfUrl);
enrichItem.title = hasHfUrl
? ''
: 'Link this model to a HuggingFace repo first (Link Model \u2192 Link to HuggingFace)';
}
handleMenuAction(action, menuItem) {
// First try to handle with common actions
if (ModelContextMenuMixin.handleCommonMenuActions.call(this, action)) {
return;
}
// Otherwise handle lora-specific actions
switch(action) {
case 'detail':
// Trigger the main card click which shows the modal
this.currentCard.click();
break;
case 'copyname':
// Generate and copy LoRA syntax
copyLoraSyntax(this.currentCard);
break;
case 'sendappend':
// Send LoRA to workflow (append mode)
this.sendLoraToWorkflow(false);
break;
case 'sendreplace':
// Send LoRA to workflow (replace mode)
this.sendLoraToWorkflow(true);
break;
case 'replace-preview':
// Add a new action for replacing preview images
getModelApiClient().replaceModelPreview(this.currentCard.dataset.filepath);
break;
case 'delete':
// Call showDeleteModal directly instead of clicking the trash button
showDeleteModal(this.currentCard.dataset.filepath);
break;
case 'move':
moveManager.showMoveModal(this.currentCard.dataset.filepath);
break;
case 'refresh-metadata':
getModelApiClient().refreshSingleModelMetadata(this.currentCard.dataset.filepath);
break;
case 'enrich-hf-llm':
this.enrichWithAgent(this.currentCard.dataset.filepath);
break;
case 'exclude':
showExcludeModal(this.currentCard.dataset.filepath);
break;
case 'restore':
this.restoreExcludedModel(this.currentCard.dataset.filepath);
break;
}
}
async enrichWithAgent(filePath) {
const { agentManager } = await import('../../managers/AgentManager.js');
const configured = await agentManager.isLlmConfigured();
if (!configured) {
showToast('toast.agent.llmNotConfigured', {}, 'warning');
return;
}
agentManager.connect();
const progressUI = state.loadingManager.showEnhancedProgress(
'Enriching metadata with AI...'
);
function cleanupCallbacks() {
const pIdx = agentManager.progressCallbacks.indexOf(onProgress);
if (pIdx >= 0) agentManager.progressCallbacks.splice(pIdx, 1);
const cIdx = agentManager.completeCallbacks.indexOf(onComplete);
if (cIdx >= 0) agentManager.completeCallbacks.splice(cIdx, 1);
const eIdx = agentManager.errorCallbacks.indexOf(onError);
if (eIdx >= 0) agentManager.errorCallbacks.splice(eIdx, 1);
}
const onProgress = (data) => {
if (data.status === 'processing' && data.current_path && data.updated_data && Object.keys(data.updated_data).length > 0) {
if (state.virtualScroller?.updateSingleItem) {
state.virtualScroller.updateSingleItem(data.current_path, data.updated_data);
}
const pct = data.total > 0 ? Math.floor((data.processed / data.total) * 100) : 0;
const name = data.current_path.split('/').pop();
progressUI.updateProgress(pct, name, `Processing ${name}`);
}
};
agentManager.onProgress(onProgress);
const onComplete = (data) => {
cleanupCallbacks();
if (data.status === 'completed') {
progressUI.complete(data.summary || 'Enrich complete');
showToast('toast.agent.enrichComplete', { summary: data.summary || 'Done' }, 'success');
}
};
agentManager.onComplete(onComplete);
const onError = (data) => {
cleanupCallbacks();
state.loadingManager.hide();
showToast('toast.agent.enrichFailed', { error: data.error || 'Unknown error' }, 'error');
};
agentManager.onError(onError);
try {
await agentManager.executeSkill('enrich_hf_metadata', [filePath]);
} catch (error) {
cleanupCallbacks();
state.loadingManager.hide();
showToast('toast.agent.enrichFailed', { error: error.message }, 'error');
}
}
sendLoraToWorkflow(replaceMode) {
const card = this.currentCard;
const usageTips = JSON.parse(card.dataset.usage_tips || '{}');
const folder = card.dataset.folder || '';
const loraName = folder ? `${folder}/${card.dataset.file_name}` : card.dataset.file_name;
const loraSyntax = buildLoraSyntax(loraName, usageTips);
sendLoraToWorkflow(loraSyntax, replaceMode, 'lora');
}
}
// Mix in shared methods
Object.assign(LoraContextMenu.prototype, ModelContextMenuMixin);