claude prompt in ui updates 1

This commit is contained in:
JasonHoku
2026-02-13 16:54:00 -08:00
parent 24acfb2ad1
commit 44da2da618
7 changed files with 884 additions and 37 deletions
+8
View File
@@ -0,0 +1,8 @@
{
"permissions": {
"allow": [
"Bash(git -C \".claude/worktrees/thirsty-greider\" status)",
"Bash(git config --global --add safe.directory '%\\(prefix\\)///DESKTOP-2GIRPC5/comfy/comfy_v0.12.3/ComfyUI/custom_nodes/ComfyUI-Ultimate-Auto-Sampler-Config-Grid-Testing-Suite/.claude/worktrees/thirsty-greider')"
]
}
}
+36 -6
View File
@@ -224,6 +224,8 @@ class UltimateConfigBuilder:
config = {
"session_name": "my_test_session",
"include_none": False,
"global_positive_groups": [],
"global_negative": "",
"config_arrays": [
{
"name": "Config 1",
@@ -235,7 +237,10 @@ class UltimateConfigBuilder:
"loras": ["None"],
"lora_omit_triggers": [],
"lora_triggerwords_append_settings": {},
"combine": True
"combine": True,
"positive_prompt_groups": [],
"negative_prompt": "",
"use_custom_prompts": False
}
]
}
@@ -474,7 +479,11 @@ class UltimateConfigBuilder:
actual_session_name = state.get("session_name", session_name)
actual_include_none = state.get("include_none", include_none)
config_arrays = state.get("config_arrays", [])
# Global prompts (used when per-config prompts are not defined)
global_positive_groups = state.get("global_positive_groups", [])
global_negative = state.get("global_negative", "")
if not config_arrays:
config_arrays = [{
"name": "Config 1",
@@ -486,7 +495,10 @@ class UltimateConfigBuilder:
"loras": ["None"],
"lora_omit_triggers": [],
"lora_triggerwords_append_settings": {},
"combine": True
"combine": True,
"positive_prompt_groups": [],
"negative_prompt": "",
"use_custom_prompts": False
}]
configs_output = []
@@ -522,12 +534,30 @@ class UltimateConfigBuilder:
# Add omit triggers if present
if omit_triggers:
config["lora_omit_triggers"] = omit_triggers
# Add trigger append settings if present
if lora_triggerwords_append_settings and any(v != "none" for v in lora_triggerwords_append_settings.values()):
config["lora_triggerwords_append_settings"] = lora_triggerwords_append_settings
# ==== PROMPT HANDLING ====
# Priority: per-config > global > node inputs (omitted = use node inputs)
use_custom = config_array.get("use_custom_prompts", False)
per_config_positive_groups = config_array.get("positive_prompt_groups", [])
per_config_negative = config_array.get("negative_prompt", "")
if use_custom and per_config_positive_groups:
# Per-config prompts override everything
# Store as nested array format for parse_prompt_input_nested() compatibility
config["positive"] = per_config_positive_groups
if per_config_negative:
config["negative"] = per_config_negative
elif global_positive_groups:
# Global prompts override node inputs
config["positive"] = global_positive_groups
if global_negative:
config["negative"] = global_negative
# If neither, omit "positive"/"negative" keys - node inputs will be used as fallback
configs_output.append(config)
json_output = json.dumps(configs_output, indent=2, ensure_ascii=False)
+56 -19
View File
@@ -260,41 +260,78 @@ def expand_lora_stack(lora_input):
def expand_configs(raw_configs, pos_prompts, neg_prompts, denoise_values, seed, extra_seeds, ckpt_name=None):
"""
Expand raw config entries into full configuration combinations.
Supports per-config prompts: if a config entry has "positive" and/or "negative" keys,
those override the external pos_prompts/neg_prompts for that entry. This allows each
config to define its own nested array prompts for Cartesian product expansion.
Args:
raw_configs: List of config dictionaries with potentially wildcarded values
pos_prompts: List of positive prompts
neg_prompts: List of negative prompts
pos_prompts: List of positive prompts (fallback when config doesn't define prompts)
neg_prompts: List of negative prompts (fallback when config doesn't define prompts)
denoise_values: List of denoise values
seed: Base seed value
extra_seeds: List of additional random seeds
ckpt_name: Checkpoint name from node input (used as default when model is "Default")
Returns:
List of fully expanded config dictionaries
"""
ALL_SCHEDULERS = comfy.samplers.KSampler.SCHEDULERS
ALL_SAMPLERS = comfy.samplers.KSampler.SAMPLERS
expanded = []
# Determine prompt pairing strategy
# Default prompt pairing strategy (used when config doesn't define its own prompts)
if len(pos_prompts) > 1 and len(neg_prompts) > 1 and len(pos_prompts) == len(neg_prompts):
print("[GridTester] Detected matching prompt lists. Using 1-to-1 Pairing.")
prompt_pairs = list(zip(pos_prompts, neg_prompts))
default_prompt_pairs = list(zip(pos_prompts, neg_prompts))
else:
prompt_pairs = list(itertools.product(pos_prompts, neg_prompts))
default_prompt_pairs = list(itertools.product(pos_prompts, neg_prompts))
def to_list(x):
return x if isinstance(x, list) else [x]
for entry in raw_configs:
# ==== PER-CONFIG PROMPT HANDLING ====
# If this config defines its own prompts, parse them and use instead of defaults
if "positive" in entry or "negative" in entry:
raw_positive = entry.get("positive", "")
raw_negative = entry.get("negative", "")
# Handle both string and already-parsed list/nested-list formats
if isinstance(raw_positive, (list, tuple)):
# Already a list/nested structure - pass as JSON string for parse_prompt_input_nested
config_pos = parse_prompt_input_nested(json.dumps(raw_positive))
elif isinstance(raw_positive, str) and raw_positive.strip():
config_pos = parse_prompt_input_nested(raw_positive)
else:
config_pos = pos_prompts # Fallback to external
if isinstance(raw_negative, (list, tuple)):
config_neg = parse_prompt_input_nested(json.dumps(raw_negative))
elif isinstance(raw_negative, str) and raw_negative.strip():
config_neg = parse_prompt_input_nested(raw_negative)
else:
config_neg = neg_prompts # Fallback to external
# Build prompt pairs for this config entry
if len(config_pos) > 1 and len(config_neg) > 1 and len(config_pos) == len(config_neg):
entry_prompt_pairs = list(zip(config_pos, config_neg))
else:
entry_prompt_pairs = list(itertools.product(config_pos, config_neg))
if entry_prompt_pairs:
print(f"[GridTester] Config defines {len(entry_prompt_pairs)} prompt combinations (overriding node inputs)")
else:
entry_prompt_pairs = default_prompt_pairs
# Expand wildcards
samplers = ALL_SAMPLERS if entry.get("sampler") == "*" else to_list(entry.get("sampler", "euler"))
schedulers = ALL_SCHEDULERS if entry.get("scheduler") == "*" else to_list(entry.get("scheduler", "normal"))
steps_l = to_list(entry.get("steps", 20))
cfgs = to_list(entry.get("cfg", 7.0))
clip_skips = to_list(entry.get("clip_skip", 0))
# Expand model folders
raw_models = to_list(entry.get("model", "Default"))
expanded_models = []
@@ -307,24 +344,24 @@ def expand_configs(raw_configs, pos_prompts, neg_prompts, denoise_values, seed,
expanded_models.append("Default")
else:
expanded_models.extend(get_files_from_folder(m, "checkpoints"))
# Expand LoRA stacks (strengths are now in the lora string itself)
expanded_loras = expand_lora_stack(entry.get("lora", "None"))
# Get LoRA trigger word omissions (if specified)
lora_omit_triggers = entry.get("lora_omit_triggers", [])
if not isinstance(lora_omit_triggers, list):
lora_omit_triggers = [lora_omit_triggers]
# Get LoRA trigger word append settings (if specified)
lora_triggerwords_append_settings = entry.get("lora_triggerwords_append_settings", {})
if not isinstance(lora_triggerwords_append_settings, dict):
lora_triggerwords_append_settings = {}
# Build all combinations
base_combos = []
for combo in itertools.product(samplers, schedulers, steps_l, cfgs, clip_skips, expanded_loras,
denoise_values, prompt_pairs, expanded_models):
for combo in itertools.product(samplers, schedulers, steps_l, cfgs, clip_skips, expanded_loras,
denoise_values, entry_prompt_pairs, expanded_models):
base_combos.append({
"sampler": combo[0],
"scheduler": combo[1],
@@ -340,7 +377,7 @@ def expand_configs(raw_configs, pos_prompts, neg_prompts, denoise_values, seed,
"lora_omit_triggers": lora_omit_triggers,
"lora_triggerwords_append_settings": lora_triggerwords_append_settings
})
# Apply base seed and extra seeds
for c in base_combos:
expanded.append(c)
@@ -348,7 +385,7 @@ def expand_configs(raw_configs, pos_prompts, neg_prompts, denoise_values, seed,
new_c = c.copy()
new_c["seed"] = extra_seed
expanded.append(new_c)
return expanded
@@ -9,7 +9,9 @@ import {
parseLoraString,
buildLoraString,
getIterationCount,
convertStateToConfigs
convertStateToConfigs,
countPromptCombinations,
expandPromptPreview
} from '/ultimate_config_sampler/js/conf_builder/conf-builder-utilities.js';
import {
@@ -1254,6 +1256,494 @@ export async function showTriggerLookupModal(node, arrayIdx) {
}
}
// =============================================================================
// --- PROMPT BUILDER COMPONENTS ---
// =============================================================================
/**
* Creates a reusable prompt group editor.
* Supports visual chip/tag mode and raw JSON mode.
* Shows live preview of Cartesian product combinations.
*
* @param {Array} groups - Nested array of prompt groups: [["a", "b"], ["c", "d"]]
* @param {Function} onChange - Callback when groups change: (newGroups) => void
* @param {string} label - Label for this editor (e.g. "Positive Prompt" or "Negative Prompt")
* @param {string} borderColor - CSS color for left border accent
* @returns {HTMLElement}
*/
function createPromptGroupEditor(groups, onChange, label, borderColor = "#0066cc") {
const container = document.createElement("div");
container.style.cssText = `display: flex; flex-direction: column; gap: 8px; width: 100%;`;
// Track mode state locally (visual vs raw)
let isRawMode = false;
// --- HEADER ---
const header = document.createElement("div");
header.style.cssText = "display: flex; justify-content: space-between; align-items: center;";
const titleEl = document.createElement("div");
titleEl.style.cssText = `font-size: 12px; font-weight: bold; color: ${borderColor};`;
titleEl.textContent = label;
header.appendChild(titleEl);
const modeToggleContainer = document.createElement("div");
modeToggleContainer.style.cssText = "display: flex; gap: 4px;";
const visualBtn = document.createElement("button");
visualBtn.className = "cb-prompt-mode-toggle active";
visualBtn.textContent = "Visual";
const rawBtn = document.createElement("button");
rawBtn.className = "cb-prompt-mode-toggle";
rawBtn.textContent = "JSON";
modeToggleContainer.appendChild(visualBtn);
modeToggleContainer.appendChild(rawBtn);
header.appendChild(modeToggleContainer);
container.appendChild(header);
// --- VISUAL MODE CONTAINER ---
const visualContainer = document.createElement("div");
visualContainer.style.cssText = "display: flex; flex-direction: column; gap: 6px;";
// --- RAW MODE CONTAINER ---
const rawContainer = document.createElement("div");
rawContainer.style.display = "none";
const rawTextarea = document.createElement("textarea");
rawTextarea.className = "cb-prompt-raw-editor";
rawTextarea.placeholder = '[["variation1", "variation2"], ["subject1", "subject2"]]';
rawTextarea.value = groups.length > 0 ? JSON.stringify(groups, null, 2) : "";
rawTextarea.onchange = () => {
try {
const parsed = JSON.parse(rawTextarea.value);
if (Array.isArray(parsed)) {
// Normalize: ensure each item is an array
const normalized = parsed.map(item =>
Array.isArray(item) ? item.map(String) : [String(item)]
);
onChange(normalized);
rawTextarea.style.borderColor = "#00aa44";
setTimeout(() => { rawTextarea.style.borderColor = "#3a3a3a"; }, 1000);
renderVisualGroups(normalized);
renderPreview(normalized);
}
} catch (e) {
rawTextarea.style.borderColor = "#cc3333";
}
};
rawContainer.appendChild(rawTextarea);
// --- MODE TOGGLE LOGIC ---
visualBtn.onclick = () => {
isRawMode = false;
visualContainer.style.display = "flex";
rawContainer.style.display = "none";
visualBtn.classList.add("active");
rawBtn.classList.remove("active");
};
rawBtn.onclick = () => {
isRawMode = true;
visualContainer.style.display = "none";
rawContainer.style.display = "block";
rawBtn.classList.add("active");
visualBtn.classList.remove("active");
// Sync raw editor with current groups
rawTextarea.value = groups.length > 0 ? JSON.stringify(groups, null, 2) : "";
};
container.appendChild(visualContainer);
container.appendChild(rawContainer);
// --- PREVIEW SECTION ---
const previewContainer = document.createElement("div");
container.appendChild(previewContainer);
function renderPreview(currentGroups) {
previewContainer.innerHTML = "";
const validGroups = (currentGroups || []).filter(g => Array.isArray(g) && g.length > 0);
if (validGroups.length === 0) return;
const count = countPromptCombinations(validGroups);
const previews = expandPromptPreview(validGroups, 20);
const previewDiv = document.createElement("div");
previewDiv.className = "cb-prompt-preview";
const countLabel = document.createElement("div");
countLabel.style.cssText = "font-weight: bold; margin-bottom: 4px; color: #00cc88;";
countLabel.textContent = `${count} combination${count !== 1 ? 's' : ''}${count > 20 ? ' (showing first 20)' : ''}:`;
previewDiv.appendChild(countLabel);
previews.forEach(preview => {
const item = document.createElement("div");
item.className = "cb-prompt-preview-item";
item.textContent = preview;
previewDiv.appendChild(item);
});
previewContainer.appendChild(previewDiv);
}
// --- VISUAL GROUP RENDERING ---
function renderVisualGroups(currentGroups) {
visualContainer.innerHTML = "";
(currentGroups || []).forEach((group, groupIdx) => {
const groupDiv = document.createElement("div");
groupDiv.className = "cb-prompt-group";
// Group header
const groupHeader = document.createElement("div");
groupHeader.className = "cb-prompt-group-header";
const groupLabel = document.createElement("span");
groupLabel.textContent = `Group ${groupIdx + 1} (${group.length} variation${group.length !== 1 ? 's' : ''})`;
groupHeader.appendChild(groupLabel);
const groupDeleteBtn = document.createElement("button");
groupDeleteBtn.className = "cb-button danger";
groupDeleteBtn.style.cssText = "padding: 1px 6px; font-size: 10px;";
groupDeleteBtn.textContent = "✖";
groupDeleteBtn.title = "Remove this group";
groupDeleteBtn.onclick = () => {
const newGroups = [...currentGroups];
newGroups.splice(groupIdx, 1);
onChange(newGroups);
renderVisualGroups(newGroups);
renderPreview(newGroups);
rawTextarea.value = newGroups.length > 0 ? JSON.stringify(newGroups, null, 2) : "";
};
groupHeader.appendChild(groupDeleteBtn);
groupDiv.appendChild(groupHeader);
// Chips container
const chipsDiv = document.createElement("div");
chipsDiv.className = "cb-prompt-chips";
group.forEach((variation, varIdx) => {
const chip = document.createElement("span");
chip.className = "cb-prompt-chip";
const chipText = document.createElement("span");
chipText.textContent = variation;
chipText.title = variation;
chip.appendChild(chipText);
const chipClose = document.createElement("span");
chipClose.className = "chip-close";
chipClose.textContent = "×";
chipClose.onclick = () => {
const newGroups = currentGroups.map(g => [...g]);
newGroups[groupIdx].splice(varIdx, 1);
// Remove group if empty
if (newGroups[groupIdx].length === 0) {
newGroups.splice(groupIdx, 1);
}
onChange(newGroups);
renderVisualGroups(newGroups);
renderPreview(newGroups);
rawTextarea.value = newGroups.length > 0 ? JSON.stringify(newGroups, null, 2) : "";
};
chip.appendChild(chipClose);
chipsDiv.appendChild(chip);
});
// Add variation input
const addVarBtn = document.createElement("button");
addVarBtn.className = "cb-prompt-add-variation";
addVarBtn.textContent = "+ variation";
addVarBtn.onclick = () => {
// Replace button with inline input
const inputContainer = document.createElement("div");
inputContainer.style.cssText = "display: inline-flex; gap: 2px;";
const input = document.createElement("input");
input.className = "cb-input";
input.style.cssText = "width: 180px; padding: 3px 6px; font-size: 12px;";
input.placeholder = "Enter prompt text...";
const confirmBtn = document.createElement("button");
confirmBtn.className = "cb-button primary";
confirmBtn.style.cssText = "padding: 2px 8px; font-size: 11px;";
confirmBtn.textContent = "Add";
const addVariation = () => {
const val = input.value.trim();
if (val) {
const newGroups = currentGroups.map(g => [...g]);
newGroups[groupIdx].push(val);
onChange(newGroups);
renderVisualGroups(newGroups);
renderPreview(newGroups);
rawTextarea.value = newGroups.length > 0 ? JSON.stringify(newGroups, null, 2) : "";
}
};
input.onkeydown = (e) => {
if (e.key === "Enter") addVariation();
if (e.key === "Escape") {
inputContainer.replaceWith(addVarBtn);
}
};
confirmBtn.onclick = addVariation;
inputContainer.appendChild(input);
inputContainer.appendChild(confirmBtn);
addVarBtn.replaceWith(inputContainer);
input.focus();
};
chipsDiv.appendChild(addVarBtn);
groupDiv.appendChild(chipsDiv);
visualContainer.appendChild(groupDiv);
});
// Add Group button
const addGroupBtn = document.createElement("button");
addGroupBtn.className = "cb-prompt-add-group-btn";
addGroupBtn.textContent = "➕ Add Prompt Group";
addGroupBtn.onclick = () => {
// Create input for the first variation of the new group
const inputContainer = document.createElement("div");
inputContainer.style.cssText = "display: flex; gap: 4px; margin-top: 4px;";
const input = document.createElement("input");
input.className = "cb-input";
input.style.cssText = "flex: 1; padding: 6px 8px; font-size: 12px;";
input.placeholder = "Enter first variation for new group...";
const confirmBtn = document.createElement("button");
confirmBtn.className = "cb-button primary";
confirmBtn.style.padding = "4px 12px";
confirmBtn.textContent = "Add Group";
const addGroup = () => {
const val = input.value.trim();
if (val) {
const newGroups = [...(currentGroups || []), [val]];
onChange(newGroups);
renderVisualGroups(newGroups);
renderPreview(newGroups);
rawTextarea.value = newGroups.length > 0 ? JSON.stringify(newGroups, null, 2) : "";
}
};
input.onkeydown = (e) => {
if (e.key === "Enter") addGroup();
if (e.key === "Escape") {
inputContainer.replaceWith(addGroupBtn);
}
};
confirmBtn.onclick = addGroup;
inputContainer.appendChild(input);
inputContainer.appendChild(confirmBtn);
addGroupBtn.replaceWith(inputContainer);
input.focus();
};
visualContainer.appendChild(addGroupBtn);
}
// Initial render
renderVisualGroups(groups);
renderPreview(groups);
return container;
}
// --- GLOBAL PROMPTS SECTION ---
export function renderGlobalPromptsSection(node, container) {
const isCollapsed = node.uiState.globalPromptsSectionCollapsed || false;
const section = document.createElement("div");
section.className = "cb-section full-width";
// Header (collapsible)
const header = document.createElement("div");
header.className = "cb-section-toggle";
header.style.cssText = "display: flex; justify-content: space-between; align-items: center; cursor: pointer; user-select: none; margin-bottom: 10px;";
const titleSpan = document.createElement("div");
titleSpan.className = "cb-section-title";
titleSpan.style.cssText = "margin-bottom: 0; border-bottom: none; padding-bottom: 0; color: #00aa44;";
titleSpan.textContent = "📝 Global Prompts (Override Node Inputs)";
header.appendChild(titleSpan);
const arrowSpan = document.createElement("span");
arrowSpan.textContent = isCollapsed ? "▶" : "▼";
arrowSpan.style.cssText = "color: #00aa44; font-size: 12px;";
header.appendChild(arrowSpan);
section.appendChild(header);
// Content container
const contentDiv = document.createElement("div");
contentDiv.style.display = isCollapsed ? "none" : "flex";
contentDiv.style.flexDirection = "column";
contentDiv.style.gap = "12px";
header.onclick = () => {
const isNowCollapsed = contentDiv.style.display !== "none";
contentDiv.style.display = isNowCollapsed ? "none" : "flex";
arrowSpan.textContent = isNowCollapsed ? "▶" : "▼";
node.uiState.globalPromptsSectionCollapsed = isNowCollapsed;
};
// Info text
const info = document.createElement("div");
info.style.cssText = "font-size: 11px; color: #888; font-style: italic;";
info.textContent = "These prompts override the sampler node's positive/negative text inputs for ALL config arrays. Per-config prompts (below) can further override these.";
contentDiv.appendChild(info);
// Positive Prompt Editor
const positiveSection = document.createElement("div");
positiveSection.className = "cb-prompt-section global";
const positiveEditor = createPromptGroupEditor(
node.state.global_positive_groups || [],
(newGroups) => {
node.state.global_positive_groups = newGroups;
node.saveState();
},
"✅ Positive Prompt Groups",
"#00aa44"
);
positiveSection.appendChild(positiveEditor);
contentDiv.appendChild(positiveSection);
// Negative Prompt (simple text area, not nested groups)
const negativeSection = document.createElement("div");
negativeSection.className = "cb-prompt-section global";
const negLabel = document.createElement("div");
negLabel.style.cssText = "font-size: 12px; font-weight: bold; color: #cc4444; margin-bottom: 6px;";
negLabel.textContent = "❌ Negative Prompt";
negativeSection.appendChild(negLabel);
const negInput = document.createElement("textarea");
negInput.className = "cb-prompt-raw-editor";
negInput.style.minHeight = "50px";
negInput.placeholder = "bad quality, worst quality, lowres";
negInput.value = node.state.global_negative || "";
negInput.onchange = () => {
node.state.global_negative = negInput.value;
node.saveState();
};
negativeSection.appendChild(negInput);
contentDiv.appendChild(negativeSection);
section.appendChild(contentDiv);
container.appendChild(section);
}
// --- PER-CONFIG PROMPTS SECTION ---
export function renderConfigPromptsSection(node, div, configArray, arrayIdx) {
const uid = `prompts_${arrayIdx}`;
const isCollapsed = node.uiState.promptsSectionCollapsed[uid] !== false; // Default collapsed
const section = document.createElement("div");
section.style.cssText = "width: 100%; margin-top: 10px; padding-top: 10px; border-top: 1px dashed #444;";
// Header
const header = document.createElement("div");
header.className = "cb-section-toggle";
header.style.cssText = "padding: 8px; background: #3a3a3a; border-radius: 4px; margin-bottom: 8px; font-weight: bold; color: #9966cc;";
const promptCount = configArray.use_custom_prompts && configArray.positive_prompt_groups
? countPromptCombinations(configArray.positive_prompt_groups)
: 0;
const titleSpan = document.createElement("span");
titleSpan.textContent = `Prompts${promptCount > 0 ? ` (${promptCount} combinations)` : ''}`;
header.appendChild(titleSpan);
const arrowSpan = document.createElement("span");
arrowSpan.textContent = isCollapsed ? "▶" : "▼";
header.appendChild(arrowSpan);
section.appendChild(header);
// Content
const contentDiv = document.createElement("div");
contentDiv.style.display = isCollapsed ? "none" : "block";
header.onclick = () => {
const isNowCollapsed = contentDiv.style.display !== "none";
contentDiv.style.display = isNowCollapsed ? "none" : "block";
arrowSpan.textContent = isNowCollapsed ? "▶" : "▼";
node.uiState.promptsSectionCollapsed[uid] = isNowCollapsed;
};
// Toggle: Use Custom Prompts
const toggleLabel = document.createElement("label");
toggleLabel.style.cssText = "display: flex; align-items: center; gap: 8px; cursor: pointer; margin-bottom: 10px; font-size: 13px;";
const toggleCheck = document.createElement("input");
toggleCheck.type = "checkbox";
toggleCheck.checked = configArray.use_custom_prompts || false;
toggleCheck.onchange = () => {
node.state.config_arrays[arrayIdx].use_custom_prompts = toggleCheck.checked;
node.saveState();
node.renderUI();
};
toggleLabel.appendChild(toggleCheck);
toggleLabel.appendChild(document.createTextNode("Use Custom Prompts (Override Global/Node)"));
contentDiv.appendChild(toggleLabel);
if (configArray.use_custom_prompts) {
// Positive Prompt Editor
const positiveSection = document.createElement("div");
positiveSection.className = "cb-prompt-section per-config";
const positiveEditor = createPromptGroupEditor(
configArray.positive_prompt_groups || [],
(newGroups) => {
node.state.config_arrays[arrayIdx].positive_prompt_groups = newGroups;
node.saveState();
},
"✅ Positive Prompt Groups",
"#9966cc"
);
positiveSection.appendChild(positiveEditor);
contentDiv.appendChild(positiveSection);
// Negative Prompt (simple text)
const negativeSection = document.createElement("div");
negativeSection.className = "cb-prompt-section per-config";
negativeSection.style.marginTop = "8px";
const negLabel = document.createElement("div");
negLabel.style.cssText = "font-size: 12px; font-weight: bold; color: #cc4444; margin-bottom: 6px;";
negLabel.textContent = "❌ Negative Prompt";
negativeSection.appendChild(negLabel);
const negInput = document.createElement("textarea");
negInput.className = "cb-prompt-raw-editor";
negInput.style.minHeight = "50px";
negInput.placeholder = "bad quality, worst quality, lowres";
negInput.value = configArray.negative_prompt || "";
negInput.onchange = () => {
node.state.config_arrays[arrayIdx].negative_prompt = negInput.value;
node.saveState();
};
negativeSection.appendChild(negInput);
contentDiv.appendChild(negativeSection);
} else {
const infoText = document.createElement("div");
infoText.style.cssText = "font-size: 11px; color: #666; font-style: italic; padding: 4px;";
infoText.textContent = "Using global prompts (or node inputs if no global prompts defined).";
contentDiv.appendChild(infoText);
}
section.appendChild(contentDiv);
div.appendChild(section);
}
// --- MAIN RENDER UI FUNCTION ---
export function renderPreviewSection(container) {
@@ -1288,6 +1778,9 @@ export async function renderUI(node, availableLoras, availableModels, loraFolder
renderConfigSection(node, topRow, availableConfigs);
root.appendChild(topRow);
// Global Prompts Section (between session/config management and config arrays)
renderGlobalPromptsSection(node, root);
const configSection = document.createElement("div");
configSection.className = "cb-section full-width";
configSection.innerHTML = '<div class="cb-section-title">⚙️ Config Arrays</div>';
@@ -1308,7 +1801,10 @@ export async function renderUI(node, availableLoras, availableModels, loraFolder
loras: ["None"],
lora_omit_triggers: [],
lora_triggerwords_append_settings: {},
combine: false
combine: false,
positive_prompt_groups: [],
negative_prompt: "",
use_custom_prompts: false
});
node.saveState();
node.renderUI();
@@ -1321,6 +1817,7 @@ export async function renderUI(node, availableLoras, availableModels, loraFolder
node.state.config_arrays.forEach((configArray, arrayIdx) => {
const arrayElement = createConfigArrayElement(node, configArray, arrayIdx);
renderConfigPromptsSection(node, arrayElement, configArray, arrayIdx);
renderModelsSection(node, arrayElement, configArray, arrayIdx, availableModels, modelFolders);
renderLorasSection(node, arrayElement, configArray, arrayIdx, availableLoras, loraFolders);
arraysContainer.appendChild(arrayElement);
+149 -1
View File
@@ -241,7 +241,155 @@ export function getStyles() {
width: 98%; /* Use more screen space on mobile */
}
}
/* --- Prompt Builder Styles --- */
.cb-prompt-section {
background: #252525;
border-radius: 4px;
padding: 10px;
margin-top: 8px;
width: 100%;
box-sizing: border-box;
}
.cb-prompt-section.global {
border-left: 3px solid #00aa44;
}
.cb-prompt-section.per-config {
border-left: 3px solid #9966cc;
}
.cb-prompt-group {
background: #1a1a1a;
border: 1px solid #3a3a3a;
border-radius: 4px;
padding: 8px;
margin-bottom: 6px;
display: flex;
flex-direction: column;
gap: 6px;
}
.cb-prompt-group-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 11px;
color: #888;
}
.cb-prompt-chips {
display: flex;
flex-wrap: wrap;
gap: 4px;
min-height: 28px;
align-items: center;
}
.cb-prompt-chip {
display: inline-flex;
align-items: center;
background: #0066cc33;
border: 1px solid #0066cc66;
color: #88ccff;
border-radius: 12px;
padding: 3px 8px;
font-size: 12px;
font-family: monospace;
gap: 4px;
max-width: 300px;
word-break: break-all;
}
.cb-prompt-chip .chip-close {
cursor: pointer;
color: #ff8888;
font-weight: bold;
font-size: 14px;
line-height: 1;
margin-left: 2px;
}
.cb-prompt-chip .chip-close:hover {
color: #ff4444;
}
.cb-prompt-preview {
background: #0a0a0a;
border: 1px solid #333;
border-radius: 4px;
padding: 8px;
margin-top: 8px;
max-height: 200px;
overflow-y: auto;
font-family: monospace;
font-size: 11px;
color: #00cc88;
}
.cb-prompt-preview-item {
padding: 2px 0;
border-bottom: 1px solid #1a1a1a;
}
.cb-prompt-preview-item:last-child {
border-bottom: none;
}
.cb-prompt-raw-editor {
width: 100%;
min-height: 80px;
background: #0a0a0a;
border: 1px solid #3a3a3a;
color: #0cc;
padding: 8px;
border-radius: 4px;
font-family: monospace;
font-size: 12px;
resize: vertical;
box-sizing: border-box;
}
.cb-prompt-raw-editor:focus {
outline: none;
border-color: #0066cc;
}
.cb-prompt-mode-toggle {
background: transparent;
border: 1px solid #555;
color: #aaa;
padding: 3px 8px;
border-radius: 3px;
cursor: pointer;
font-size: 10px;
}
.cb-prompt-mode-toggle:hover {
background: #333;
color: #fff;
}
.cb-prompt-mode-toggle.active {
background: #0066cc33;
border-color: #0066cc;
color: #88ccff;
}
.cb-prompt-add-group-btn {
width: 100%;
border: 1px dashed #555;
background: rgba(0,102,204,0.1);
color: #88ccff;
padding: 6px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
margin-top: 4px;
}
.cb-prompt-add-group-btn:hover {
background: rgba(0,102,204,0.2);
border-color: #0066cc;
}
.cb-prompt-add-variation {
background: transparent;
border: 1px dashed #444;
color: #888;
padding: 3px 8px;
border-radius: 12px;
cursor: pointer;
font-size: 11px;
}
.cb-prompt-add-variation:hover {
background: #333;
color: #ccc;
border-color: #666;
}
</style>
`;
}
+111 -5
View File
@@ -175,6 +175,48 @@ export function buildLoraString(name, modelStr, clipStr) {
return `${norm}:${modelStr.toFixed(2)}:${clipStr.toFixed(2)}`;
}
// --- PROMPT HELPER FUNCTIONS ---
/**
* Count the number of Cartesian product combinations from nested prompt groups.
* Each group is an array of variations. The total is the product of all group sizes.
* Example: [["a", "b"], ["c"]] = 2 * 1 = 2 combinations
*/
export function countPromptCombinations(groups) {
if (!groups || !Array.isArray(groups) || groups.length === 0) return 1;
// Filter out empty groups
const validGroups = groups.filter(g => Array.isArray(g) && g.length > 0);
if (validGroups.length === 0) return 1;
return validGroups.reduce((total, group) => total * group.length, 1);
}
/**
* Generate preview of expanded prompt combinations from nested groups.
* Returns array of strings, capped at `limit` entries.
* Example: [["a", "b"], ["1", "2"]] -> ["a, 1", "a, 2", "b, 1", "b, 2"]
*/
export function expandPromptPreview(groups, limit = 20) {
if (!groups || !Array.isArray(groups) || groups.length === 0) return [];
const validGroups = groups.filter(g => Array.isArray(g) && g.length > 0);
if (validGroups.length === 0) return [];
// Iterative Cartesian product with early cutoff
let combinations = [[]];
for (const group of validGroups) {
const newCombinations = [];
for (const existing of combinations) {
for (const item of group) {
newCombinations.push([...existing, item]);
if (newCombinations.length > limit) {
return newCombinations.slice(0, limit).map(combo => combo.join(", "));
}
}
}
combinations = newCombinations;
}
return combinations.map(combo => combo.join(", "));
}
// --- ITERATION COUNT CALCULATION ---
export function getIterationCount(configArray) {
@@ -238,7 +280,13 @@ export function getIterationCount(configArray) {
}
if (l_count === 0) l_count = 1;
return m_count * l_count * s_count * sch_count * st_count * c_count;
// 4. Prompts (per-config custom prompts multiply the iteration count)
let p_count = 1;
if (configArray.use_custom_prompts && configArray.positive_prompt_groups && configArray.positive_prompt_groups.length > 0) {
p_count = countPromptCombinations(configArray.positive_prompt_groups);
}
return m_count * l_count * s_count * sch_count * st_count * c_count * p_count;
}
// --- CONFIG CONVERSION ---
@@ -247,10 +295,14 @@ export function convertStateToConfigs(state) {
const configs = [];
const split = (str) => str.split(",").map(s => s.trim()).filter(s => s);
// Global prompts from state (used when per-config prompts not set)
const globalPositiveGroups = state.global_positive_groups || [];
const globalNegative = state.global_negative || "";
state.config_arrays.forEach(configArray => {
// Process LoRAs - FIXED VERSION
let loras = configArray.loras.filter(l => l && l !== "None");
// Convert loras array to proper format
let loraValue;
if (loras.length === 0) {
@@ -302,6 +354,20 @@ export function convertStateToConfigs(state) {
config.lora_strength_lock = configArray.lora_strength_lock;
}
// ==== PROMPT HANDLING ====
// Priority: per-config > global > omit (node inputs used as fallback)
if (configArray.use_custom_prompts && configArray.positive_prompt_groups && configArray.positive_prompt_groups.length > 0) {
config.positive = configArray.positive_prompt_groups;
if (configArray.negative_prompt) {
config.negative = configArray.negative_prompt;
}
} else if (globalPositiveGroups.length > 0) {
config.positive = globalPositiveGroups;
if (globalNegative) {
config.negative = globalNegative;
}
}
configs.push(config);
});
return configs;
@@ -321,7 +387,10 @@ export function convertConfigsToConfigArrays(configs) {
lora_triggerwords_append_settings: {},
lora_bypass_states: {},
lora_strength_lock: {},
combine: false
combine: false,
positive_prompt_groups: [],
negative_prompt: "",
use_custom_prompts: false
}];
}
@@ -383,6 +452,37 @@ export function convertConfigsToConfigArrays(configs) {
strengthLock = { ...config.lora_strength_lock };
}
// Parse prompt fields from loaded config
let positivePromptGroups = [];
let negativePrompt = "";
let useCustomPrompts = false;
if (config.positive) {
// Config has per-config prompts
useCustomPrompts = true;
if (Array.isArray(config.positive)) {
// Could be nested array [["a", "b"], ["c"]] or simple array ["a", "b"]
if (config.positive.length > 0 && Array.isArray(config.positive[0])) {
// Nested array format - use as-is
positivePromptGroups = config.positive;
} else {
// Simple array - wrap as single group
positivePromptGroups = [config.positive];
}
} else if (typeof config.positive === 'string' && config.positive.trim()) {
// Plain string - wrap as single variation in single group
positivePromptGroups = [[config.positive]];
}
}
if (config.negative) {
if (typeof config.negative === 'string') {
negativePrompt = config.negative;
} else if (Array.isArray(config.negative)) {
negativePrompt = JSON.stringify(config.negative);
}
}
configArrays.push({
name: `Loaded Config ${idx + 1}`,
samplers: toString(config.sampler || "euler"),
@@ -395,7 +495,10 @@ export function convertConfigsToConfigArrays(configs) {
lora_triggerwords_append_settings: triggerPlacements,
lora_bypass_states: bypassStates,
lora_strength_lock: strengthLock,
combine: hasCombined
combine: hasCombined,
positive_prompt_groups: positivePromptGroups,
negative_prompt: negativePrompt,
use_custom_prompts: useCustomPrompts
});
});
@@ -411,6 +514,9 @@ export function convertConfigsToConfigArrays(configs) {
lora_triggerwords_append_settings: {},
lora_bypass_states: {},
lora_strength_lock: {},
combine: false
combine: false,
positive_prompt_groups: [],
negative_prompt: "",
use_custom_prompts: false
}];
}
+25 -4
View File
@@ -66,7 +66,9 @@ app.registerExtension({
modelsSectionCollapsed: {},
lorasSectionCollapsed: {},
modelsCollapsed: {},
lorasCollapsed: {}
lorasCollapsed: {},
promptsSectionCollapsed: {},
globalPromptsSectionCollapsed: false
};
// Initialize default state structure
@@ -75,6 +77,8 @@ app.registerExtension({
config_name: "default_config",
auto_save: false,
include_none: false,
global_positive_groups: [],
global_negative: "",
config_arrays: [{
name: "Config 1",
samplers: "euler, dpmpp_2m",
@@ -87,7 +91,10 @@ app.registerExtension({
lora_triggerwords_append_settings: {},
lora_bypass_states: {},
lora_strength_lock: {},
combine: false
combine: false,
positive_prompt_groups: [],
negative_prompt: "",
use_custom_prompts: false
}]
};
@@ -248,6 +255,8 @@ app.registerExtension({
config_name: "default_config",
auto_save: false,
include_none: oldState.include_none !== undefined ? oldState.include_none : false,
global_positive_groups: [],
global_negative: "",
config_arrays: arrays.map(arr => ({
name: arr.name,
samplers: oldState.samplers || "euler",
@@ -260,7 +269,10 @@ app.registerExtension({
lora_triggerwords_append_settings: {},
lora_bypass_states: {},
lora_strength_lock: {},
combine: arr.combine || false
combine: arr.combine || false,
positive_prompt_groups: [],
negative_prompt: "",
use_custom_prompts: false
}))
};
};
@@ -281,6 +293,10 @@ app.registerExtension({
if (!this.state.config_name) this.state.config_name = "default_config";
if (this.state.auto_save === undefined) this.state.auto_save = false;
// Migration: ensure global prompt fields exist
if (!this.state.global_positive_groups) this.state.global_positive_groups = [];
if (this.state.global_negative === undefined) this.state.global_negative = "";
// Migration logic requiring utilities
this.state.config_arrays.forEach(arr => {
if (arr.model && !arr.models) {
@@ -293,12 +309,17 @@ app.registerExtension({
const p = utilities.parseLoraString(l);
return utilities.buildLoraString(p.name, p.model_str, p.clip_str);
}) : ["None"];
// Ensure keys exist
if (!arr.lora_omit_triggers) arr.lora_omit_triggers = [];
if (!arr.lora_triggerwords_append_settings) arr.lora_triggerwords_append_settings = {};
if (!arr.lora_bypass_states) arr.lora_bypass_states = {};
if (!arr.lora_strength_lock) arr.lora_strength_lock = {};
// Migration: ensure prompt fields exist
if (!arr.positive_prompt_groups) arr.positive_prompt_groups = [];
if (arr.negative_prompt === undefined) arr.negative_prompt = "";
if (arr.use_custom_prompts === undefined) arr.use_custom_prompts = false;
});
} else if (existing.lora_config) {
this.state = this.migrateOldFormat(existing);