mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-06-25 12:31:15 -03:00
feat: send gen params to workflow with visual cues
- Add genParamsMapper.js: sampler/scheduler display→internal mapping, combined-name parsing, widget matching - Add sendGenParamsToWorkflow() in uiHelpers.js: resolves sampler, fetches registry by send_gen_params marker, sends via update-node-widget - Add send-params-btn UI in showcase hover panel and recipe modal - Add flashWidget() in workflow_registry.js: text-color visual cue on updated widget values (Vue: inline style + CSS, canvas: property shadow) - Add silent option to sendWidgetValueToNodes for consolidated toast - Normalize param display labels (cfg_scale→CFG, etc.) in recipe modal - Add 33 tests for genParamsMapper; update existing test assertions
This commit is contained in:
@@ -229,6 +229,19 @@
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Header row for params section */
|
||||
.metadata-row.params-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.metadata-row.params-row .param-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
/* Styling for parameters tags */
|
||||
.params-tags {
|
||||
display: flex;
|
||||
@@ -315,7 +328,8 @@
|
||||
}
|
||||
|
||||
.copy-prompt-btn,
|
||||
.send-prompt-btn {
|
||||
.send-prompt-btn,
|
||||
.send-params-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-color);
|
||||
@@ -328,7 +342,8 @@
|
||||
}
|
||||
|
||||
.copy-prompt-btn:hover,
|
||||
.send-prompt-btn:hover {
|
||||
.send-prompt-btn:hover,
|
||||
.send-params-btn:hover {
|
||||
opacity: 1;
|
||||
color: var(--lora-accent);
|
||||
background: var(--lora-surface);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Recipe Modal Component
|
||||
import { showToast, copyToClipboard, sendLoraToWorkflow, sendModelPathToWorkflow, openCivitaiByMetadata, stripLoraTags, sendPromptToWorkflow } from '../utils/uiHelpers.js';
|
||||
import { showToast, copyToClipboard, sendLoraToWorkflow, sendModelPathToWorkflow, openCivitaiByMetadata, stripLoraTags, sendPromptToWorkflow, sendGenParamsToWorkflow } 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';
|
||||
@@ -40,6 +40,16 @@ const GEN_PARAM_NORMALIZATION = {
|
||||
'Denoising strength': 'denoising_strength',
|
||||
};
|
||||
|
||||
const PARAM_DISPLAY_NAMES = {
|
||||
steps: 'Steps',
|
||||
sampler: 'Sampler',
|
||||
cfg_scale: 'CFG',
|
||||
seed: 'Seed',
|
||||
size: 'Size',
|
||||
clip_skip: 'Clip Skip',
|
||||
denoising_strength: 'Denoising Strength',
|
||||
};
|
||||
|
||||
class RecipeModal {
|
||||
constructor() {
|
||||
this.promptEditorState = {};
|
||||
@@ -588,10 +598,11 @@ class RecipeModal {
|
||||
|
||||
for (const [key, value] of Object.entries(sanitizedGenParams)) {
|
||||
if (!excludedParams.includes(key) && value !== undefined && value !== null) {
|
||||
const displayName = PARAM_DISPLAY_NAMES[key] || key;
|
||||
const paramTag = document.createElement('div');
|
||||
paramTag.className = 'param-tag';
|
||||
paramTag.innerHTML = `
|
||||
<span class="param-name">${key}:</span>
|
||||
<span class="param-name">${displayName}:</span>
|
||||
<span class="param-value">${value}</span>
|
||||
`;
|
||||
otherParamsElement.appendChild(paramTag);
|
||||
@@ -1234,6 +1245,19 @@ class RecipeModal {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Send params to workflow button
|
||||
const sendParamsBtn = document.getElementById('sendParamsBtn');
|
||||
if (sendParamsBtn) {
|
||||
sendParamsBtn.addEventListener('click', () => {
|
||||
const genParams = this.currentRecipe?.gen_params || {};
|
||||
if (!genParams || Object.keys(genParams).length === 0) {
|
||||
showToast('No generation parameters available', {}, 'warning');
|
||||
return;
|
||||
}
|
||||
sendGenParamsToWorkflow(genParams);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Media-specific utility functions for showcase components
|
||||
* (Moved from uiHelpers.js to better organize code)
|
||||
*/
|
||||
import { showToast, copyToClipboard, getNSFWLevelName, sendPromptToWorkflow, stripLoraTags } from '../../../utils/uiHelpers.js';
|
||||
import { showToast, copyToClipboard, getNSFWLevelName, sendPromptToWorkflow, stripLoraTags, sendGenParamsToWorkflow } from '../../../utils/uiHelpers.js';
|
||||
import { state } from '../../../state/index.js';
|
||||
import { getModelApiClient } from '../../../api/modelApiFactory.js';
|
||||
import { NSFW_LEVELS, getMatureBlurThreshold } from '../../../utils/constants.js';
|
||||
@@ -344,6 +344,48 @@ export function initMetadataPanelHandlers(container) {
|
||||
});
|
||||
});
|
||||
|
||||
// Handle send params buttons
|
||||
const paramsBtn = metadataPanel.querySelector('.send-params-btn');
|
||||
if (paramsBtn) {
|
||||
paramsBtn.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
// Collect gen params from the param-tag elements
|
||||
const tagsContainer = wrapper.querySelector('.params-tags');
|
||||
if (!tagsContainer) return;
|
||||
|
||||
const paramTags = tagsContainer.querySelectorAll('.param-tag');
|
||||
const genParams = {};
|
||||
|
||||
// Map display labels to genParams keys
|
||||
const labelToKey = {
|
||||
'Seed': 'seed',
|
||||
'Steps': 'steps',
|
||||
'Sampler': 'sampler',
|
||||
'CFG': 'cfg_scale',
|
||||
};
|
||||
|
||||
paramTags.forEach(tag => {
|
||||
const nameEl = tag.querySelector('.param-name');
|
||||
const valueEl = tag.querySelector('.param-value');
|
||||
if (!nameEl || !valueEl) return;
|
||||
|
||||
const label = nameEl.textContent.replace(':', '').trim();
|
||||
const key = labelToKey[label];
|
||||
if (key) {
|
||||
genParams[key] = valueEl.textContent.trim();
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(genParams).length === 0) {
|
||||
showToast('No sendable parameters found', {}, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
await sendGenParamsToWorkflow(genParams);
|
||||
});
|
||||
}
|
||||
|
||||
// Prevent panel scroll from causing modal scroll
|
||||
metadataPanel.addEventListener('wheel', (e) => {
|
||||
const isAtTop = metadataPanel.scrollTop === 0;
|
||||
|
||||
@@ -28,14 +28,24 @@ export function generateMetadataPanel(hasParams, hasPrompts, prompt, negativePro
|
||||
|
||||
if (hasParams) {
|
||||
content += `
|
||||
<div class="params-tags">
|
||||
${size ? `<div class="param-tag"><span class="param-name">Size:</span><span class="param-value">${size}</span></div>` : ''}
|
||||
${seed ? `<div class="param-tag"><span class="param-name">Seed:</span><span class="param-value">${seed}</span></div>` : ''}
|
||||
${model ? `<div class="param-tag"><span class="param-name">Model:</span><span class="param-value">${model}</span></div>` : ''}
|
||||
${steps ? `<div class="param-tag"><span class="param-name">Steps:</span><span class="param-value">${steps}</span></div>` : ''}
|
||||
${sampler ? `<div class="param-tag"><span class="param-name">Sampler:</span><span class="param-value">${sampler}</span></div>` : ''}
|
||||
${cfgScale ? `<div class="param-tag"><span class="param-name">CFG:</span><span class="param-value">${cfgScale}</span></div>` : ''}
|
||||
${clipSkip ? `<div class="param-tag"><span class="param-name">Clip Skip:</span><span class="param-value">${clipSkip}</span></div>` : ''}
|
||||
<div class="metadata-row params-row">
|
||||
<div class="param-header">
|
||||
<span class="metadata-label">Params:</span>
|
||||
<div class="param-actions">
|
||||
<button class="send-params-btn" title="Send Params to Workflow">
|
||||
<i class="fas fa-paper-plane"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="params-tags">
|
||||
${size ? `<div class="param-tag"><span class="param-name">Size:</span><span class="param-value">${size}</span></div>` : ''}
|
||||
${seed ? `<div class="param-tag"><span class="param-name">Seed:</span><span class="param-value">${seed}</span></div>` : ''}
|
||||
${model ? `<div class="param-tag"><span class="param-name">Model:</span><span class="param-value">${model}</span></div>` : ''}
|
||||
${steps ? `<div class="param-tag"><span class="param-name">Steps:</span><span class="param-value">${steps}</span></div>` : ''}
|
||||
${sampler ? `<div class="param-tag"><span class="param-name">Sampler:</span><span class="param-value">${sampler}</span></div>` : ''}
|
||||
${cfgScale ? `<div class="param-tag"><span class="param-name">CFG:</span><span class="param-value">${cfgScale}</span></div>` : ''}
|
||||
${clipSkip ? `<div class="param-tag"><span class="param-name">Clip Skip:</span><span class="param-value">${clipSkip}</span></div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
296
static/js/utils/genParamsMapper.js
Normal file
296
static/js/utils/genParamsMapper.js
Normal file
@@ -0,0 +1,296 @@
|
||||
/**
|
||||
* genParamsMapper.js
|
||||
* Maps display/recipe generation parameter values (sampler, scheduler) to
|
||||
* ComfyUI internal widget values, enabling "Send Gen Params to Workflow".
|
||||
*
|
||||
* Strategy (3 layers):
|
||||
* 1. Direct lookup via SAMPLER_DISPLAY_TO_INTERNAL
|
||||
* 2. Combined-name parsing (e.g. "Euler a Karras" → sampler + scheduler)
|
||||
* 3. Graceful skip for model-specific / unrecognized values
|
||||
*/
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sampler display name → internal name (ComfyUI KSampler.SAMPLERS / SAMPLER_NAMES)
|
||||
// ---------------------------------------------------------------------------
|
||||
const SAMPLER_DISPLAY_TO_INTERNAL = {
|
||||
// --- Euler family ---
|
||||
'Euler': 'euler',
|
||||
'euler': 'euler',
|
||||
'Euler a': 'euler_ancestral',
|
||||
'Euler A': 'euler_ancestral',
|
||||
'Euler ancestral': 'euler_ancestral',
|
||||
'Euler Ancestral': 'euler_ancestral',
|
||||
'euler_ancestral': 'euler_ancestral',
|
||||
|
||||
// --- Heun ---
|
||||
'Heun': 'heun',
|
||||
'heun': 'heun',
|
||||
'Heun++': 'heunpp2',
|
||||
'heunpp2': 'heunpp2',
|
||||
|
||||
// --- DPM2 ---
|
||||
'DPM2': 'dpm_2',
|
||||
'DPM 2': 'dpm_2',
|
||||
'dpm_2': 'dpm_2',
|
||||
'DPM2 a': 'dpm_2_ancestral',
|
||||
'DPM2 Ancestral': 'dpm_2_ancestral',
|
||||
'dpm_2_ancestral': 'dpm_2_ancestral',
|
||||
|
||||
// --- LMS ---
|
||||
'LMS': 'lms',
|
||||
'lms': 'lms',
|
||||
|
||||
// --- DPM fast / adaptive ---
|
||||
'DPM fast': 'dpm_fast',
|
||||
'DPM Fast': 'dpm_fast',
|
||||
'dpm_fast': 'dpm_fast',
|
||||
'DPM adaptive': 'dpm_adaptive',
|
||||
'DPM Adaptive': 'dpm_adaptive',
|
||||
'dpm_adaptive': 'dpm_adaptive',
|
||||
|
||||
// --- DPM++ 2S ancestral ---
|
||||
'DPM++ 2S a': 'dpmpp_2s_ancestral',
|
||||
'DPM++ 2S A': 'dpmpp_2s_ancestral',
|
||||
'DPM++ 2S Ancestral': 'dpmpp_2s_ancestral',
|
||||
'dpmpp_2s_ancestral': 'dpmpp_2s_ancestral',
|
||||
|
||||
// --- DPM++ SDE ---
|
||||
'DPM++ SDE': 'dpmpp_sde',
|
||||
'dpmpp_sde': 'dpmpp_sde',
|
||||
|
||||
// --- DPM++ 2M ---
|
||||
'DPM++ 2M': 'dpmpp_2m',
|
||||
'dpmpp_2m': 'dpmpp_2m',
|
||||
|
||||
// --- DPM++ 2M SDE ---
|
||||
'DPM++ 2M SDE': 'dpmpp_2m_sde',
|
||||
'dpmpp_2m_sde': 'dpmpp_2m_sde',
|
||||
|
||||
// --- DPM++ 3M SDE ---
|
||||
'DPM++ 3M SDE': 'dpmpp_3m_sde',
|
||||
'dpmpp_3m_sde': 'dpmpp_3m_sde',
|
||||
|
||||
// --- Others ---
|
||||
'DDIM': 'ddim',
|
||||
'ddim': 'ddim',
|
||||
'DDPM': 'ddpm',
|
||||
'ddpm': 'ddpm',
|
||||
'LCM': 'lcm',
|
||||
'lcm': 'lcm',
|
||||
'IPNDM': 'ipndm',
|
||||
'ipndm': 'ipndm',
|
||||
'DEIS': 'deis',
|
||||
'deis': 'deis',
|
||||
'UniPC': 'uni_pc',
|
||||
'unipc': 'uni_pc',
|
||||
'uni_pc': 'uni_pc',
|
||||
|
||||
// --- Restart / res_multistep ---
|
||||
'Restart': 'res_multistep',
|
||||
'res_multistep': 'res_multistep',
|
||||
|
||||
// --- ER SDE ---
|
||||
'ER SDE': 'er_sde',
|
||||
'E-R SDE': 'er_sde',
|
||||
'er_sde': 'er_sde',
|
||||
|
||||
// --- SA Solver ---
|
||||
'SA Solver': 'sa_solver',
|
||||
'SA solver': 'sa_solver',
|
||||
'sa_solver': 'sa_solver',
|
||||
|
||||
// --- Seeds ---
|
||||
'Seeds 2': 'seeds_2',
|
||||
'seeds_2': 'seeds_2',
|
||||
'Seeds 3': 'seeds_3',
|
||||
'seeds_3': 'seeds_3',
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Known scheduler suffixes (ComfyUI KSampler.SCHEDULERS)
|
||||
// Sorted by length (descending) for longest-match-first parsing.
|
||||
// ---------------------------------------------------------------------------
|
||||
const SCHEDULER_SUFFIXES = [
|
||||
'sgm_uniform',
|
||||
'ddim_uniform',
|
||||
'linear_quadratic',
|
||||
'kl_optimal',
|
||||
'exponential',
|
||||
'karras',
|
||||
'simple',
|
||||
'normal',
|
||||
'beta',
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Scheduler-only values (values that are schedulers, not samplers)
|
||||
// ---------------------------------------------------------------------------
|
||||
const SCHEDULER_ONLY_VALUES = new Set([
|
||||
'simple', 'sgm_uniform', 'karras', 'exponential',
|
||||
'ddim_uniform', 'beta', 'normal', 'linear_quadratic', 'kl_optimal',
|
||||
]);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Param key → widget name candidates (searched in order)
|
||||
// ---------------------------------------------------------------------------
|
||||
const PARAM_TO_WIDGET_CANDIDATES = {
|
||||
seed: ['seed', 'noise_seed'],
|
||||
steps: ['steps'],
|
||||
cfg: ['cfg'],
|
||||
sampler: ['sampler_name', 'sampler'],
|
||||
scheduler: ['scheduler'],
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Parse a combined sampler+scheduler value (space-separated or underscore)
|
||||
// e.g., "Euler a Karras", "DPM++ 2M beta", "er_sde_beta"
|
||||
// Returns { sampler: internalName|null, scheduler: internalName|null } or null
|
||||
// ---------------------------------------------------------------------------
|
||||
function parseCombinedSamplerName(rawValue) {
|
||||
if (!rawValue || typeof rawValue !== 'string') return null;
|
||||
const trimmed = rawValue.trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
// Try space-separated first: split on last space
|
||||
const spaceIdx = trimmed.lastIndexOf(' ');
|
||||
if (spaceIdx > 0) {
|
||||
const candidateScheduler = trimmed.slice(spaceIdx + 1).trim().toLowerCase();
|
||||
if (SCHEDULER_SUFFIXES.includes(candidateScheduler)) {
|
||||
const samplerPart = trimmed.slice(0, spaceIdx).trim();
|
||||
const internalSampler = SAMPLER_DISPLAY_TO_INTERNAL[samplerPart];
|
||||
if (internalSampler) {
|
||||
return { sampler: internalSampler, scheduler: candidateScheduler };
|
||||
}
|
||||
// samplerPart might be a combined name itself (e.g., "DPM++ 2M SDE")
|
||||
// Try recursing (one level max) — already handled since we split at last space
|
||||
}
|
||||
}
|
||||
|
||||
// Try underscore-separated: e.g., "er_sde_beta"
|
||||
const underIdx = trimmed.lastIndexOf('_');
|
||||
if (underIdx > 0) {
|
||||
const candidateScheduler = trimmed.slice(underIdx + 1).trim().toLowerCase();
|
||||
if (SCHEDULER_SUFFIXES.includes(candidateScheduler)) {
|
||||
const samplerPart = trimmed.slice(0, underIdx).trim();
|
||||
const internalSampler = SAMPLER_DISPLAY_TO_INTERNAL[samplerPart] || SAMPLER_DISPLAY_TO_INTERNAL[samplerPart.toLowerCase()];
|
||||
if (internalSampler) {
|
||||
return { sampler: internalSampler, scheduler: candidateScheduler };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main resolver: takes a raw sampler value from recipe/showcase metadata
|
||||
// and returns { sampler: internalName|null, scheduler: internalName|null }
|
||||
// ---------------------------------------------------------------------------
|
||||
function resolveSamplerScheduler(rawValue) {
|
||||
if (!rawValue || typeof rawValue !== 'string') {
|
||||
return { sampler: null, scheduler: null };
|
||||
}
|
||||
|
||||
const trimmed = rawValue.trim();
|
||||
if (!trimmed) return { sampler: null, scheduler: null };
|
||||
|
||||
// 1. Try direct lookup first
|
||||
const direct = SAMPLER_DISPLAY_TO_INTERNAL[trimmed];
|
||||
if (direct) return { sampler: direct, scheduler: null };
|
||||
|
||||
// 2. Try lowercase direct lookup
|
||||
const lowerDirect = SAMPLER_DISPLAY_TO_INTERNAL[trimmed.toLowerCase()];
|
||||
if (lowerDirect) return { sampler: lowerDirect, scheduler: null };
|
||||
|
||||
// 3. Scheduler-only value? (check BEFORE the "already internal name" regex,
|
||||
// because scheduler values like "karras", "simple" also match that pattern)
|
||||
if (SCHEDULER_ONLY_VALUES.has(trimmed.toLowerCase())) {
|
||||
return { sampler: null, scheduler: trimmed.toLowerCase() };
|
||||
}
|
||||
|
||||
// 4. Already an internal name? (lowercase, no spaces)
|
||||
if (/^[a-z][a-z0-9_]+$/.test(trimmed)) {
|
||||
return { sampler: trimmed, scheduler: null };
|
||||
}
|
||||
|
||||
// 5. Try combined name parsing (space-separated or underscore)
|
||||
const combined = parseCombinedSamplerName(trimmed);
|
||||
if (combined) return combined;
|
||||
|
||||
// 6. Custom format like "multistep/dpmpp_2m_simple" — try extracting the last segment
|
||||
if (trimmed.includes('/')) {
|
||||
const parts = trimmed.split('/');
|
||||
const last = parts[parts.length - 1];
|
||||
if (last) {
|
||||
const subResult = resolveSamplerScheduler(last);
|
||||
if (subResult.sampler || subResult.scheduler) return subResult;
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Unrecognized — return null for both
|
||||
return { sampler: null, scheduler: null };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Find which gen params can be sent to a given node, matching by widget names
|
||||
// Returns array of { widgetName, value } objects
|
||||
// ---------------------------------------------------------------------------
|
||||
function findMatchingWidgets(nodeWidgetNames, resolvedParams) {
|
||||
if (!nodeWidgetNames || !Array.isArray(nodeWidgetNames) || nodeWidgetNames.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const widgetSet = new Set(nodeWidgetNames.map(w => String(w).toLowerCase()));
|
||||
const updates = [];
|
||||
|
||||
// Simple numeric/string params: seed, steps, cfg
|
||||
const simpleParams = [
|
||||
{ key: 'seed', value: resolvedParams.seed },
|
||||
{ key: 'steps', value: resolvedParams.steps },
|
||||
{ key: 'cfg', value: resolvedParams.cfg },
|
||||
];
|
||||
for (const { key, value } of simpleParams) {
|
||||
if (value === undefined || value === null || value === '') continue;
|
||||
const candidates = PARAM_TO_WIDGET_CANDIDATES[key] || [key];
|
||||
for (const candidate of candidates) {
|
||||
if (widgetSet.has(candidate.toLowerCase())) {
|
||||
updates.push({ widgetName: candidate, value: String(value) });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sampler
|
||||
if (resolvedParams.sampler) {
|
||||
const candidates = PARAM_TO_WIDGET_CANDIDATES.sampler;
|
||||
for (const candidate of candidates) {
|
||||
if (widgetSet.has(candidate.toLowerCase())) {
|
||||
updates.push({ widgetName: candidate, value: resolvedParams.sampler });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scheduler
|
||||
if (resolvedParams.scheduler) {
|
||||
const candidates = PARAM_TO_WIDGET_CANDIDATES.scheduler;
|
||||
for (const candidate of candidates) {
|
||||
if (widgetSet.has(candidate.toLowerCase())) {
|
||||
updates.push({ widgetName: candidate, value: resolvedParams.scheduler });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updates;
|
||||
}
|
||||
|
||||
export {
|
||||
SAMPLER_DISPLAY_TO_INTERNAL,
|
||||
SCHEDULER_SUFFIXES,
|
||||
SCHEDULER_ONLY_VALUES,
|
||||
PARAM_TO_WIDGET_CANDIDATES,
|
||||
parseCombinedSamplerName,
|
||||
resolveSamplerScheduler,
|
||||
findMatchingWidgets,
|
||||
};
|
||||
@@ -6,6 +6,7 @@ import { eventManager } from './EventManager.js';
|
||||
import { bannerService } from '../managers/BannerService.js';
|
||||
import { modalManager } from '../managers/ModalManager.js';
|
||||
import { buildCivitaiUrl, normalizeCivitaiPageHost } from './civitaiUtils.js';
|
||||
import { resolveSamplerScheduler, findMatchingWidgets } from './genParamsMapper.js';
|
||||
|
||||
const CIVITAI_HOST_INFO_BANNER_ID = 'civitai-host-preference';
|
||||
const CIVITAI_HOST_INFO_BANNER_SEEN_KEY = 'civitai_host_info_banner_seen';
|
||||
@@ -856,11 +857,12 @@ async function sendWidgetValueToNodes(nodeIds, nodesMap, widgetName, value, mess
|
||||
successMessage = 'Updated workflow node',
|
||||
failureMessage = 'Failed to update workflow node',
|
||||
missingTargetMessage = 'No target node selected',
|
||||
silent = false,
|
||||
} = messages;
|
||||
|
||||
const targetIds = Array.isArray(nodeIds) ? nodeIds : [];
|
||||
if (targetIds.length === 0) {
|
||||
showToast(missingTargetMessage, {}, 'warning');
|
||||
if (!silent) showToast(missingTargetMessage, {}, 'warning');
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -869,7 +871,7 @@ async function sendWidgetValueToNodes(nodeIds, nodesMap, widgetName, value, mess
|
||||
.filter((reference) => reference && reference.node_id !== undefined);
|
||||
|
||||
if (references.length === 0) {
|
||||
showToast(missingTargetMessage, {}, 'warning');
|
||||
if (!silent) showToast(missingTargetMessage, {}, 'warning');
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -888,16 +890,16 @@ async function sendWidgetValueToNodes(nodeIds, nodesMap, widgetName, value, mess
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
showToast(successMessage, {}, 'success');
|
||||
if (!silent) showToast(successMessage, {}, 'success');
|
||||
return true;
|
||||
}
|
||||
|
||||
const errorMessage = result?.error || failureMessage;
|
||||
showToast(errorMessage, {}, 'error');
|
||||
if (!silent) showToast(errorMessage, {}, 'error');
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('Failed to send widget value to workflow:', error);
|
||||
showToast(failureMessage, {}, 'error');
|
||||
if (!silent) showToast(failureMessage, {}, 'error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1056,6 +1058,127 @@ export async function sendPromptToWorkflow(promptText, options = {}) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send generation parameters (seed, steps, cfg, sampler, scheduler) to
|
||||
* workflow nodes that have been marked with "Send Gen Params Target".
|
||||
*
|
||||
* @param {Object} genParams - Raw gen_params from recipe or showcase metadata
|
||||
* @returns {Promise<boolean>} Whether the send succeeded
|
||||
*/
|
||||
export async function sendGenParamsToWorkflow(genParams) {
|
||||
if (!genParams || typeof genParams !== 'object') {
|
||||
showToast('No generation parameters to send', {}, 'warning');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 1. Extract relevant params (skip prompt, negative_prompt, clip_skip, denoising_strength)
|
||||
const raw = {
|
||||
seed: genParams.seed,
|
||||
steps: genParams.steps,
|
||||
cfg: genParams.cfg_scale,
|
||||
};
|
||||
|
||||
// 2. Resolve sampler/scheduler
|
||||
const resolved = resolveSamplerScheduler(genParams.sampler);
|
||||
if (resolved) {
|
||||
if (resolved.sampler) raw.sampler = resolved.sampler;
|
||||
if (resolved.scheduler) raw.scheduler = resolved.scheduler;
|
||||
}
|
||||
|
||||
// Check if we have anything to send
|
||||
const hasAny = Object.values(raw).some(v => v !== undefined && v !== null && v !== '');
|
||||
if (!hasAny) {
|
||||
showToast('No sendable parameters found', {}, 'warning');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. Fetch workflow registry
|
||||
const registry = await fetchWorkflowRegistry();
|
||||
if (!registry) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. Filter nodes by marker_role === "send_gen_params"
|
||||
const targetNodes = filterRegistryNodes(registry.nodes, (node) => {
|
||||
return node.marker_role === 'send_gen_params' && isNodeEnabled(node);
|
||||
});
|
||||
|
||||
const nodeKeys = Object.keys(targetNodes);
|
||||
if (nodeKeys.length === 0) {
|
||||
showToast(
|
||||
'No node marked as Send Gen Params Target.\nRight-click a node in ComfyUI → Mark as → Send Gen Params Target',
|
||||
{},
|
||||
'warning'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 5. For each candidate node, find matching widgets
|
||||
// Also collect widget_names from registry for matching
|
||||
const sendToNode = async (nodeIds) => {
|
||||
const targetIds = Array.isArray(nodeIds) ? nodeIds : [nodeIds];
|
||||
let allSuccess = true;
|
||||
let totalSent = 0;
|
||||
let totalFailed = 0;
|
||||
|
||||
for (const nodeKey of targetIds) {
|
||||
const node = targetNodes[nodeKey];
|
||||
if (!node) continue;
|
||||
|
||||
const widgetNames = node.widget_names || [];
|
||||
const updates = findMatchingWidgets(widgetNames, raw);
|
||||
|
||||
if (updates.length === 0) {
|
||||
showToast(`Node "${node.title || node.type}" has no matching widgets for these parameters`, {}, 'warning');
|
||||
allSuccess = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Send each widget value sequentially
|
||||
for (const update of updates) {
|
||||
const success = await sendWidgetValueToNodes(
|
||||
[nodeKey],
|
||||
targetNodes,
|
||||
update.widgetName,
|
||||
update.value,
|
||||
{
|
||||
silent: true,
|
||||
}
|
||||
);
|
||||
if (success) {
|
||||
totalSent++;
|
||||
} else {
|
||||
totalFailed++;
|
||||
allSuccess = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show single summary toast
|
||||
if (totalSent > 0 && totalFailed === 0) {
|
||||
showToast(`Sent ${totalSent} parameter${totalSent > 1 ? 's' : ''} to workflow`, {}, 'success');
|
||||
} else if (totalFailed > 0 && totalSent > 0) {
|
||||
showToast(`Partially updated (${totalSent} ok, ${totalFailed} failed)`, {}, 'warning');
|
||||
} else if (totalFailed > 0) {
|
||||
showToast('Failed to update parameters', {}, 'error');
|
||||
}
|
||||
return allSuccess;
|
||||
};
|
||||
|
||||
// 6. If multiple nodes, show node selector; otherwise send directly
|
||||
if (nodeKeys.length === 1) {
|
||||
return await sendToNode([nodeKeys[0]]);
|
||||
}
|
||||
|
||||
showNodeSelector(targetNodes, {
|
||||
actionType: 'Gen Params',
|
||||
actionMode: 'Update',
|
||||
onSend: sendToNode,
|
||||
enableSendAll: true,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// Global variable to track active node selector state
|
||||
let nodeSelectorState = {
|
||||
isActive: false,
|
||||
|
||||
Reference in New Issue
Block a user