Files
StableLlama-ComfyUI-basic_d…/web/js/dynamicnode.js
T
StableLlama 4af3a0628b Fix dynamic node inputs lost when loading workflows from images/videos (#64)
ComfyUI embeds a differently serialized copy of the workflow into the
images/videos a prompt produces: graphToPrompt() runs
compressWidgetInputSlots() on it, which removes every widget input slot
that has no link and no label - a leftover from legacy widget conversion.
For the dynamic nodes of this pack, key_1/value_1/key_2/... only exist
because the JS created those slots, so they were stripped while their
values stayed in widgets_values. On reload only the connected slots
survived, the node was rebuilt as key_0/value_0/value_2 plus one fresh
empty pair, and the positional widget restoration shifted every value
into the wrong widget - the DICT came back with only its first key.

Label dynamically created widget inputs with their own name, so they are
kept in the compressed workflow. This is a visual no-op, as the widget
shows its name anyway. Workflows saved before the label existed carry
none, so onConfigure stamps it on every restored dynamic widget input.
Renumbering now keeps input and widget labels in sync with the new name.

Verified in ComfyUI 0.43 (frontend 1.57.4): loading the PNG produced by
the minimal workflow of the report restores all key/value pairs, that
recreated workflow runs, and clearing a pair still renumbers correctly.
2026-11-03 23:32:20 +01:00

705 lines
32 KiB
JavaScript

/**
* ComfyUI basic data handling.
* Copyright (C) 2025 StableLlama
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Purpose: Manage dynamic input nodes in ComfyUI.
* Inspired by cozy_ex_dynamic
*/
import { app } from "../../../scripts/app.js";
const TypeSlot = {
Input: 1,
Output: 2
};
const TypeSlotEvent = {
Connect: true,
Disconnect: false
};
// Helper for escaping strings to be used in RegExp
const escapeRegExp = (string) => {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
};
app.registerExtension({
name: 'Basic data handling: dynamic input',
async beforeRegisterNodeDef(nodeType, nodeData, appInstance) {
if (!nodeType.comfyClass.startsWith('Basic data handling:')) {
return;
}
const combinedInputData = {
...(nodeData?.input?.required ?? {}),
...(nodeData?.input?.optional ?? {})
};
let combinedInputDataOrder = [];
if (nodeData.input_order) {
if (nodeData.input_order.required) combinedInputDataOrder.push(...nodeData.input_order.required);
if (nodeData.input_order.optional) combinedInputDataOrder.push(...nodeData.input_order.optional);
} else if (nodeData.input) {
if (nodeData.input.required) combinedInputDataOrder.push(...Object.keys(nodeData.input.required));
if (nodeData.input.optional) combinedInputDataOrder.push(...Object.keys(nodeData.input.optional));
}
const dynamicInputs = [];
const dynamicInputGroups = [];
for (const name of combinedInputDataOrder) {
if (!combinedInputData[name]) continue;
const dynamicSetting = combinedInputData[name][1]?._dynamic;
const dynamicGroup = combinedInputData[name][1]?._dynamicGroup ?? 0;
if (dynamicSetting) { // Only process for inputs marked with _dynamic
const inputOptions = combinedInputData[name][1] || {};
const dynamicComfyType = combinedInputData[name][0];
let isActuallyWidget = false;
let effectiveWidgetGuiType;
if (inputOptions.widget?.type) {
isActuallyWidget = true;
effectiveWidgetGuiType = inputOptions.widget.type;
} else if (inputOptions.widget && typeof inputOptions.widget === 'string') {
isActuallyWidget = true;
effectiveWidgetGuiType = inputOptions.widget;
}
else if (inputOptions._widgetType) {
isActuallyWidget = true;
effectiveWidgetGuiType = inputOptions._widgetType;
} else if (inputOptions.widgetType) {
isActuallyWidget = true;
effectiveWidgetGuiType = inputOptions.widgetType;
}
else {
const comfyTypeUpper = String(dynamicComfyType).toUpperCase();
const implicitWidgetGuiTypesMap = {
"STRING": "text", "INT": "number", "FLOAT": "number",
"NUMBER": "number", "BOOLEAN": "toggle"
};
if (implicitWidgetGuiTypesMap[comfyTypeUpper] && inputOptions.forceInput !== true && dynamicComfyType !== '*') {
isActuallyWidget = true;
effectiveWidgetGuiType = implicitWidgetGuiTypesMap[comfyTypeUpper];
} else if (comfyTypeUpper === "COMBO" && inputOptions.values) {
isActuallyWidget = true;
effectiveWidgetGuiType = "combo";
} else {
isActuallyWidget = false;
effectiveWidgetGuiType = dynamicComfyType;
}
}
let determinedBaseName;
let suffixExtractionMatcher;
if (dynamicSetting === 'number') {
const r = /^(.*?)(\d+)$/;
const m = name.match(r);
determinedBaseName = (m && m[1] !== undefined) ? m[1] : name;
suffixExtractionMatcher = new RegExp(`^(${escapeRegExp(determinedBaseName)})(\\d*)$`);
} else if (dynamicSetting === 'letter') {
const r = /^(.*?)([a-zA-Z])$/;
const m = name.match(r);
determinedBaseName = (m && m[1] !== undefined) ? m[1] : name;
suffixExtractionMatcher = new RegExp(`^(${escapeRegExp(determinedBaseName)})([a-zA-Z])$`);
} else {
console.warn(`[Basic Data Handling] Unknown dynamic type: ${dynamicSetting} for input ${name} on node ${nodeType.comfyClass}`);
continue;
}
if (dynamicInputGroups[dynamicGroup] === undefined) {
dynamicInputGroups[dynamicGroup] = [];
}
dynamicInputGroups[dynamicGroup].push(dynamicInputs.length);
dynamicInputs.push({
name,
baseName: determinedBaseName,
matcher: suffixExtractionMatcher,
dynamic: dynamicSetting,
dynamicComfyType,
dynamicGroup,
isWidget: isActuallyWidget,
actualWidgetGuiType: effectiveWidgetGuiType,
originalOptions: inputOptions
});
}
}
if (dynamicInputs.length === 0) {
return;
}
const getDynamicInputDefinition = (inputName) => {
for (const di of dynamicInputs) {
const m = inputName.match(di.matcher);
if (m && m[1] === di.baseName) {
return { definition: di, suffix: m[2] || "" };
}
}
return null;
};
const isDynamicInput = (inputName) => !!getDynamicInputDefinition(inputName);
const updateSlotIndices = (node) => {
node.inputs.forEach((input, index) => {
input.slot_index = index;
if (input.link !== null && input.link !== undefined) {
const linkInfo = node.graph.links[input.link];
if (linkInfo) {
linkInfo.target_slot = index;
}
}
});
};
const getWidgetDefaultValue = (widgetDef) => { // widgetDef = { type: actualWidgetGuiType, options: originalOptions }
const type = widgetDef.type;
const opts = widgetDef.options || {};
switch (String(type).toLowerCase()) {
case 'number':
return opts.default ?? 0;
case 'combo':
return opts.default ?? (opts.values && opts.values.length > 0 ? opts.values[0] : '');
case 'text':
case 'string':
return opts.default ?? '';
case 'toggle':
case 'boolean':
return opts.default ?? false;
default:
return opts.default ?? '';
}
};
const isDynamicInputEmpty = (node, inputIndex) => {
const input = node.inputs[inputIndex];
if (input.link !== null && input.link !== undefined) return false;
const diData = getDynamicInputDefinition(input.name);
if (diData && diData.definition.isWidget) {
if (input.widget && input.widget.name) {
const widget = node.widgets.find(w => w.name === input.widget.name);
if (widget) {
return widget.value === getWidgetDefaultValue({
type: diData.definition.actualWidgetGuiType,
options: diData.definition.originalOptions
});
} else {
return true;
}
} else {
return true;
}
}
return true;
};
const removeWidgetForInput = (node, inputIdx) => {
const widgetName = node.inputs[inputIdx]?.widget?.name;
if (!widgetName) return;
const widget = node.widgets?.find((w) => w.name === widgetName);
if (!widget) return;
// Use the node API so ComfyUI's widget value store is cleaned up as well.
// A plain array splice leaves a stale store entry behind which blocks the
// rename in renumberDynamicInputs, desyncing widget names from input names.
if (typeof node.removeWidget === "function") {
node.removeWidget(widget);
} else {
const widgetIdx = node.widgets.indexOf(widget);
if (widgetIdx !== -1) {
node.widgets.splice(widgetIdx, 1);
}
}
};
nodeType.prototype.getDynamicGroup = function(inputName) {
const diData = getDynamicInputDefinition(inputName);
return diData ? diData.definition.dynamicGroup : undefined;
};
const addStandardWidget = function(name, widgetGuiType, defaultValue, fullConfigOptions = {}) {
let currentVal = defaultValue;
const widgetSpecificOpts = { ...fullConfigOptions };
if (String(widgetGuiType).toLowerCase() === "combo") {
if (!widgetSpecificOpts.values && Array.isArray(fullConfigOptions.default)) {
widgetSpecificOpts.values = fullConfigOptions.default;
} else if (!widgetSpecificOpts.values && fullConfigOptions.options?.values) {
widgetSpecificOpts.values = fullConfigOptions.options.values;
}
if (!widgetSpecificOpts.values) {
widgetSpecificOpts.values = [currentVal];
}
if (widgetSpecificOpts.values && !widgetSpecificOpts.values.includes(currentVal) && widgetSpecificOpts.values.length > 0) {
currentVal = widgetSpecificOpts.values[0];
}
}
return this.addWidget(widgetGuiType, name, currentVal, () => {}, widgetSpecificOpts);
};
nodeType.prototype.addInputAtPosition = function (name, comfyInputType, widgetGuiType, position, isWidgetFlag, shape, widgetDefaultVal, widgetConfOpts) {
if (isWidgetFlag) {
addStandardWidget.call(this, name, widgetGuiType, widgetDefaultVal, widgetConfOpts);
this.addInput(name, comfyInputType, { shape, widget: { name } });
// ComfyUI drops widget inputs that are not connected from the workflow it
// embeds into saved images/videos (compressWidgetInputSlots) unless the slot
// carries a label. For dynamic inputs the slot is what keeps the whole
// key/value pair alive, so label it with its own name to survive the round trip.
this.inputs[this.inputs.length - 1].label = name;
} else {
this.addInput(name, comfyInputType, { shape });
}
const newInputIndex = this.inputs.length - 1;
if (position < newInputIndex && position < this.inputs.length -1 ) {
const newInput = this.inputs.pop();
this.inputs.splice(position, 0, newInput);
}
updateSlotIndices(this);
return this.inputs[position];
};
let isProcessingConnection = false;
const generateDynamicInputName = (dynamicBehavior, baseName, count) => {
if (dynamicBehavior === 'letter') {
return `${baseName}${String.fromCharCode(97 + count)}`;
} else { // 'number'
return `${baseName}${count}`;
}
};
nodeType.prototype.renumberDynamicInputs = function(baseNameToRenumber, dynamicBehavior) {
const inputsToRenumber = [];
for (let i = 0; i < this.inputs.length; i++) {
const input = this.inputs[i];
const diData = getDynamicInputDefinition(input.name);
if (diData && diData.definition.baseName === baseNameToRenumber && diData.definition.dynamic === dynamicBehavior) {
inputsToRenumber.push({
inputRef: input,
widgetName: input.widget ? input.widget.name : null
});
}
}
for (let i = 0; i < inputsToRenumber.length; i++) {
const { inputRef, widgetName } = inputsToRenumber[i];
const newName = generateDynamicInputName(dynamicBehavior, baseNameToRenumber, i);
if (inputRef.name === newName) continue;
if (widgetName) {
const widget = this.widgets.find(w => w.name === widgetName);
if (widget) {
widget.name = newName;
if (widget.label) widget.label = newName;
}
if (inputRef.widget) inputRef.widget.name = newName;
}
inputRef.name = newName;
if (inputRef.label) inputRef.label = newName;
}
updateSlotIndices(this);
};
const handleEmptyDynamicInput = function() { // (Content largely unchanged from previous good version, ensure it uses new DI props if needed)
let overallChangesMade = false;
let scanAgainPass = true;
while (scanAgainPass) {
scanAgainPass = false;
const dynamicGroupInfo = new Map();
for (let i = 0; i < this.inputs.length; i++) {
const currentInput = this.inputs[i];
const diData = getDynamicInputDefinition(currentInput.name);
if (!diData) continue;
const { definition: di, suffix } = diData;
const group = di.dynamicGroup;
if (!dynamicGroupInfo.has(group)) {
dynamicGroupInfo.set(group, { items: new Map() });
}
const groupData = dynamicGroupInfo.get(group);
if (!groupData.items.has(suffix)) {
groupData.items.set(suffix, { inputsInfo: [], allEmpty: true });
}
const itemData = groupData.items.get(suffix);
itemData.inputsInfo.push({ diDefinition: di, originalInput: currentInput, originalIndex: i });
if (!isDynamicInputEmpty(this, i)) { // isDynamicInputEmpty uses new DI props
itemData.allEmpty = false;
}
}
for (const [groupId, groupData] of dynamicGroupInfo) {
const { items } = groupData;
if (items.size === 0) continue;
const sortedItemSuffixes = Array.from(items.keys()).sort((a, b) => {
const numA = parseInt(a, 10);
const numB = parseInt(b, 10);
if (!isNaN(numA) && !isNaN(numB)) return numA - numB;
if (a.length !== b.length) return a.length - b.length;
return String(a).localeCompare(String(b));
});
let activeItemCount = 0;
const emptyItemSuffixes = [];
for (const suffix of sortedItemSuffixes) {
if (items.get(suffix).allEmpty) {
emptyItemSuffixes.push(suffix);
} else {
activeItemCount++;
}
}
if (emptyItemSuffixes.length === 0) continue;
let placeholderSuffix = null;
if (activeItemCount === 0 && sortedItemSuffixes.length > 0) {
placeholderSuffix = sortedItemSuffixes[0];
} else if (activeItemCount > 0 && emptyItemSuffixes.length > 0) {
placeholderSuffix = emptyItemSuffixes[emptyItemSuffixes.length - 1];
}
const inputsToRemoveDetails = [];
for (const suffix of emptyItemSuffixes) {
if (suffix !== placeholderSuffix) {
const itemData = items.get(suffix);
for (const inputDetail of itemData.inputsInfo) {
inputsToRemoveDetails.push(inputDetail);
}
}
}
if (inputsToRemoveDetails.length > 0) {
inputsToRemoveDetails.sort((a, b) => b.originalIndex - a.originalIndex);
for (const { originalInput, originalIndex } of inputsToRemoveDetails) {
if (this.inputs[originalIndex] === originalInput) {
removeWidgetForInput(this, originalIndex);
this.removeInput(originalIndex);
scanAgainPass = true;
overallChangesMade = true;
} else {
scanAgainPass = true;
overallChangesMade = true;
}
}
if (scanAgainPass) break;
}
}
if (scanAgainPass) continue;
}
if (overallChangesMade) {
const renumberedBaseNames = new Set();
for (const groupIdx_str in dynamicInputGroups) {
const groupIdx = parseInt(groupIdx_str, 10);
if (dynamicInputGroups[groupIdx]) {
for (const memberIdx of dynamicInputGroups[groupIdx]) {
const { baseName, dynamic } = dynamicInputs[memberIdx]; // 'dynamic' here is behavior ('number'/'letter')
if (!renumberedBaseNames.has(baseName + dynamic)) { // Ensure unique combination
this.renumberDynamicInputs(baseName, dynamic);
renumberedBaseNames.add(baseName + dynamic);
}
}
}
}
this.setDirtyCanvas(true, true);
}
};
const handleDynamicInputActivation = function(dynamicGroup) {
const { slots: dynamicSlotsFromGetter } = this.getDynamicSlots();
for (const slot of dynamicSlotsFromGetter) {
if (slot.dynamicGroup === dynamicGroup && this.widgets &&
!this.widgets.some((w) => w.name === slot.name)) {
const diData = getDynamicInputDefinition(slot.name);
if (diData && diData.definition.isWidget) { // Use our enhanced definition
const originalInputDef = diData.definition;
addStandardWidget.call(this, slot.name,
originalInputDef.actualWidgetGuiType,
getWidgetDefaultValue({ type: originalInputDef.actualWidgetGuiType, options: originalInputDef.originalOptions }),
originalInputDef.originalOptions
);
}
}
}
let allExistingItemsActive = true;
const itemSuffixesInGroup = new Set();
const groupSlots = dynamicSlotsFromGetter.filter(s => s.dynamicGroup === dynamicGroup);
groupSlots.forEach(s => {
const diData = getDynamicInputDefinition(s.name);
if (diData) itemSuffixesInGroup.add(diData.suffix);
});
if (itemSuffixesInGroup.size === 0 && dynamicInputGroups[dynamicGroup]?.length > 0) {
allExistingItemsActive = true;
} else {
let hasCompletelyEmptyItem = false;
for (const suffix of itemSuffixesInGroup) {
const inputsOfThisItem = groupSlots.filter(s => {
const diData = getDynamicInputDefinition(s.name);
return diData && diData.suffix === suffix;
});
if (inputsOfThisItem.length > 0 && inputsOfThisItem.every(s => !s.connected)) { // s.connected from getDynamicSlots
hasCompletelyEmptyItem = true;
break;
}
}
allExistingItemsActive = !hasCompletelyEmptyItem;
}
if (allExistingItemsActive) {
let lastDynamicIdx = -1;
if (groupSlots.length > 0) {
lastDynamicIdx = Math.max(...groupSlots.map(slot => slot.index));
} else {
let maxIdx = -1;
for(const di of dynamicInputs){ // di is a full definition object
if(di.dynamicGroup < dynamicGroup) {
for(let i=this.inputs.length-1; i>=0; --i){
if(this.inputs[i].name.startsWith(di.baseName)){
maxIdx = Math.max(maxIdx, i);
break;
}
}
}
}
lastDynamicIdx = (maxIdx === -1) ? (this.inputs.length -1) : maxIdx;
}
this.addNewDynamicInputForGroup(dynamicGroup, lastDynamicIdx);
}
this.setDirtyCanvas(true, true);
};
// --- Event Handlers (onConnectionsChange, onConnectInput, onConfigure, onRemoved, onWidgetChanged) ---
const onConnectionsChange = nodeType.prototype.onConnectionsChange;
nodeType.prototype.onConnectionsChange = function (type, slotIndex, isConnected, link, ioSlot) {
const originalReturn = onConnectionsChange?.apply(this, arguments);
if (type === TypeSlot.Input && slotIndex < this.inputs.length && this.inputs[slotIndex] && isDynamicInput(this.inputs[slotIndex].name)) {
if (isProcessingConnection) return originalReturn;
isProcessingConnection = true;
const dynamicGroup = this.getDynamicGroup(this.inputs[slotIndex].name);
if (dynamicGroup !== undefined) {
if (isConnected === TypeSlotEvent.Connect) {
handleDynamicInputActivation.call(this, dynamicGroup);
} else if (isConnected === TypeSlotEvent.Disconnect) {
handleEmptyDynamicInput.call(this);
}
}
isProcessingConnection = false;
}
return originalReturn;
};
const onConnectInput = nodeType.prototype.onConnectInput;
nodeType.prototype.onConnectInput = function(inputIndex, outputType, outputSlot, outputNode, outputIndex) {
const result = onConnectInput?.apply(this, arguments) ?? true;
if (this.inputs[inputIndex].link !== null && this.inputs[inputIndex].link !== undefined ) {
const pre_isProcessingConnection = isProcessingConnection;
isProcessingConnection = true;
this.disconnectInput(inputIndex, true);
isProcessingConnection = pre_isProcessingConnection;
}
return result;
}
const onConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function () {
const originalReturn = onConfigure?.apply(this, arguments);
// Inputs restored from a workflow may not carry the label yet, e.g. when the
// workflow was saved before it was introduced. Without the label ComfyUI
// drops the slot again when the workflow is embedded into an image/video, so
// stamp it here for every dynamic widget input.
for (const input of this.inputs ?? []) {
if (!input.widget || input.label || !isDynamicInput(input.name)) continue;
input.label = input.name;
const widget = this.widgets?.find((w) => w.name === input.widget.name);
if (widget && !widget.label) widget.label = input.name;
}
return originalReturn;
};
const onRemoved = nodeType.prototype.onRemoved;
nodeType.prototype.onRemoved = function () {
if(!isProcessingConnection){
isProcessingConnection = true;
for (let i = this.inputs.length - 1; i >= 0; i--) {
if (this.inputs[i].link !== null && this.inputs[i].link !== undefined) {
this.disconnectInput(i, true);
}
}
isProcessingConnection = false;
}
return onRemoved?.apply(this, arguments);
}
const onWidgetChanged = nodeType.prototype.onWidgetChanged;
nodeType.prototype.onWidgetChanged = function (widgetName, newValue, oldValue, widgetObject) {
const originalReturn = onWidgetChanged?.apply(this, arguments);
if (isProcessingConnection || !isDynamicInput(widgetName)) {
return originalReturn;
}
const dynamicGroup = this.getDynamicGroup(widgetName);
if (dynamicGroup === undefined) return originalReturn;
const diData = getDynamicInputDefinition(widgetName);
const defaultValue = diData ? getWidgetDefaultValue({type: diData.definition.actualWidgetGuiType, options: diData.definition.originalOptions}) : getWidgetDefaultValue(widgetObject) /*fallback*/;
const wasEffectivelyEmpty = oldValue === defaultValue;
const isNowEffectivelyEmpty = newValue === defaultValue;
if (wasEffectivelyEmpty === isNowEffectivelyEmpty) return originalReturn;
isProcessingConnection = true;
if (wasEffectivelyEmpty && !isNowEffectivelyEmpty) {
handleDynamicInputActivation.call(this, dynamicGroup);
} else if (!wasEffectivelyEmpty && isNowEffectivelyEmpty) {
handleEmptyDynamicInput.call(this);
}
isProcessingConnection = false;
return originalReturn;
};
nodeType.prototype.getDynamicSlots = function(filterDynamicGroup = null) { // (Content largely unchanged, relies on isDynamicInputEmpty which is updated)
const dynamicSlotsResult = [];
const dynamicGroupCount = {};
const dynamicGroupConnected = {};
const itemsState = new Map();
for (const [index, input] of this.inputs.entries()) {
const diData = getDynamicInputDefinition(input.name);
if (!diData) continue;
const { definition: di, suffix } = diData;
const currentDynamicGroup = di.dynamicGroup;
if (filterDynamicGroup !== null && currentDynamicGroup !== filterDynamicGroup) continue;
if (!itemsState.has(currentDynamicGroup)) itemsState.set(currentDynamicGroup, new Map());
const groupItems = itemsState.get(currentDynamicGroup);
if (!groupItems.has(suffix)) groupItems.set(suffix, { isActive: false, inputCount: 0, activeInputCount: 0 });
const itemState = groupItems.get(suffix);
itemState.inputCount++;
const isInputActive = !isDynamicInputEmpty(this, index); // Uses updated isDynamicInputEmpty
if (isInputActive) itemState.activeInputCount++;
dynamicSlotsResult.push({
index, name: input.name, isWidget: input.widget !== undefined, shape: input.shape,
connected: isInputActive,
isDynamic: true, dynamicGroup: currentDynamicGroup,
});
}
for(const [groupId, groupItems] of itemsState) {
dynamicGroupCount[groupId] = groupItems.size;
dynamicGroupConnected[groupId] = [];
const sortedSuffixes = Array.from(groupItems.keys()).sort((a,b) => {
const numA = parseInt(a,10); const numB = parseInt(b,10);
if(!isNaN(numA) && !isNaN(numB)) return numA-numB;
if(a.length !== b.length) return a.length - b.length;
return String(a).localeCompare(String(b));
});
for(const suffix of sortedSuffixes){
const item = groupItems.get(suffix);
dynamicGroupConnected[groupId].push(item.activeInputCount > 0);
}
}
return { slots: dynamicSlotsResult, groupCount: dynamicGroupCount, groupConnected: dynamicGroupConnected };
};
nodeType.prototype.addNewDynamicInputForGroup = function(dynamicGroup, lastKnownInputIndexInGroup) {
let insertPosition = lastKnownInputIndexInGroup + 1;
let inputInRange = true;
const groupMemberDefinitions = dynamicInputGroups[dynamicGroup].map(idx => dynamicInputs[idx]);
const { slots: currentGroupSlots } = this.getDynamicSlots(dynamicGroup);
const existingSuffixes = new Set();
currentGroupSlots.forEach(s => {
const diData = getDynamicInputDefinition(s.name);
if(diData) existingSuffixes.add(diData.suffix);
});
let newItemNumericSuffix = 0;
// Find the smallest non-negative integer not in existingSuffixes (if they are numbers)
// or just use existingSuffixes.size if complex/letter based for simplicity (renumbering will fix)
// For simplicity here, using size, assuming renumbering will handle actual ordering.
// A more robust suffix generation might be needed if strict ordering before renumbering is critical.
const isNumericSuffix = groupMemberDefinitions.length > 0 && groupMemberDefinitions[0].dynamic === 'number';
if (isNumericSuffix) {
while (existingSuffixes.has(String(newItemNumericSuffix))) {
newItemNumericSuffix++;
}
} else { // letter or complex
newItemNumericSuffix = existingSuffixes.size; // This is the 'count' for letter generation
}
for (const diDefinition of groupMemberDefinitions) {
// Use the new properties from diDefinition
const { baseName, dynamic, dynamicComfyType, isWidget, actualWidgetGuiType, originalOptions } = diDefinition;
if (dynamic === 'letter' && newItemNumericSuffix >= 26) {
inputInRange = false; continue;
}
const newName = generateDynamicInputName(dynamic, baseName, newItemNumericSuffix);
const refSlot = currentGroupSlots.find(s => s.name.startsWith(baseName)) || currentGroupSlots[0];
const widgetDefault = getWidgetDefaultValue({ type: actualWidgetGuiType, options: originalOptions });
this.addInputAtPosition(
newName,
dynamicComfyType, // ComfyUI input type (e.g., "STRING", "*")
actualWidgetGuiType, // GUI widget type (e.g., "text", "number", "combo")
insertPosition++,
isWidget, // The crucial boolean
refSlot?.shape,
widgetDefault,
originalOptions // Full original options for widget config
);
}
return inputInRange;
};
}
});