fix(nodes): preserve autocomplete widget values across workflow restore

This commit is contained in:
Will Miao
2026-03-29 19:25:30 +08:00
parent ca44c367b3
commit a4cb51e96c
4 changed files with 288 additions and 19 deletions

View File

@@ -139,7 +139,7 @@ const onWheel = (event: WheelEvent) => {
} }
// Handle external value changes (e.g., from "send lora to workflow") // Handle external value changes (e.g., from "send lora to workflow")
const onExternalValueChange = (event: CustomEvent<{ value: string }>) => { const onExternalValueChange = () => {
updateHasTextState() updateHasTextState()
} }

View File

@@ -24,6 +24,8 @@ const JSON_DISPLAY_WIDGET_MIN_WIDTH = 300
const JSON_DISPLAY_WIDGET_MIN_HEIGHT = 200 const JSON_DISPLAY_WIDGET_MIN_HEIGHT = 200
const AUTOCOMPLETE_TEXT_WIDGET_MIN_HEIGHT = 60 const AUTOCOMPLETE_TEXT_WIDGET_MIN_HEIGHT = 60
const AUTOCOMPLETE_TEXT_WIDGET_MAX_HEIGHT = 100 const AUTOCOMPLETE_TEXT_WIDGET_MAX_HEIGHT = 100
const AUTOCOMPLETE_METADATA_VERSION = 1
const LORA_MANAGER_WIDGET_IDS_PROPERTY = '__lm_widget_ids'
// @ts-ignore - ComfyUI external module // @ts-ignore - ComfyUI external module
import { app } from '../../../scripts/app.js' import { app } from '../../../scripts/app.js'
@@ -373,6 +375,136 @@ function createJsonDisplayWidget(node) {
// Store nodeData options per widget type for autocomplete widgets // Store nodeData options per widget type for autocomplete widgets
const widgetInputOptions: Map<string, { placeholder?: string }> = new Map() const widgetInputOptions: Map<string, { placeholder?: string }> = new Map()
function getSerializableWidgetNames(node: any): string[] {
return (node.widgets || [])
.filter((widget: any) => widget && widget.serialize !== false)
.map((widget: any) => widget.name)
}
function createAutocompleteMetadataValue(textWidgetName = 'text') {
return {
version: AUTOCOMPLETE_METADATA_VERSION,
textWidgetName
}
}
function shouldBypassAutocompleteWidgetMigration(
node: any,
widgetValues: unknown[]
): boolean {
const inputDefs = node?.constructor?.nodeData?.inputs
if (!inputDefs || !Array.isArray(widgetValues)) {
return false
}
const widgetNames = new Set((node.widgets || []).map((widget: any) => widget?.name))
const hasAutocompleteMetadataWidget = Array.from(widgetNames).some((name) =>
typeof name === 'string' && name.startsWith('__lm_autocomplete_meta_')
)
if (!hasAutocompleteMetadataWidget) {
return false
}
const originalWidgetsInputs = Object.values(inputDefs).filter((input: any) =>
widgetNames.has(input.name) || input.forceInput
)
const widgetIndexHasForceInput = originalWidgetsInputs.flatMap((input: any) =>
input.control_after_generate
? [!!input.forceInput, false]
: [!!input.forceInput]
)
return (
widgetIndexHasForceInput.some(Boolean) &&
widgetIndexHasForceInput.length === widgetValues.length
)
}
function remapWidgetValuesByName(
widgetValues: unknown[],
savedWidgetNames: string[],
currentWidgetNames: string[]
): unknown[] {
const valueByName = new Map<string, unknown>()
savedWidgetNames.forEach((name, index) => {
if (index < widgetValues.length) {
valueByName.set(name, widgetValues[index])
}
})
const remappedValues: unknown[] = []
for (const name of currentWidgetNames) {
if (valueByName.has(name)) {
remappedValues.push(valueByName.get(name))
}
}
return remappedValues
}
function injectDefaultAutocompleteMetadataValues(
widgetValues: unknown[],
currentWidgetNames: string[]
): unknown[] {
const repairedValues: unknown[] = []
let legacyValueIndex = 0
for (const widgetName of currentWidgetNames) {
if (widgetName.startsWith('__lm_autocomplete_meta_')) {
const textWidgetName = widgetName.replace('__lm_autocomplete_meta_', '') || 'text'
repairedValues.push(createAutocompleteMetadataValue(textWidgetName))
continue
}
if (legacyValueIndex < widgetValues.length) {
repairedValues.push(widgetValues[legacyValueIndex])
legacyValueIndex++
}
}
return repairedValues
}
function normalizeAutocompleteWidgetValues(node: any, info: any) {
if (!info || !Array.isArray(info.widgets_values)) {
return
}
const currentWidgetNames = getSerializableWidgetNames(node)
if (currentWidgetNames.length === 0) {
return
}
const savedWidgetNames = info.properties?.[LORA_MANAGER_WIDGET_IDS_PROPERTY]
if (Array.isArray(savedWidgetNames) && savedWidgetNames.length > 0) {
const remappedValues = remapWidgetValuesByName(
info.widgets_values,
savedWidgetNames,
currentWidgetNames
)
info.widgets_values = remappedValues
return
}
const metadataWidgetCount = currentWidgetNames.filter((name) =>
name.startsWith('__lm_autocomplete_meta_')
).length
if (
metadataWidgetCount > 0 &&
info.widgets_values.length === currentWidgetNames.length - metadataWidgetCount
) {
const repairedValues = injectDefaultAutocompleteMetadataValues(
info.widgets_values,
currentWidgetNames
)
info.widgets_values = repairedValues
}
}
// Listen for Vue DOM mode setting changes and dispatch custom event // Listen for Vue DOM mode setting changes and dispatch custom event
const initVueDomModeListener = () => { const initVueDomModeListener = () => {
if (app.ui?.settings?.addEventListener) { if (app.ui?.settings?.addEventListener) {
@@ -429,9 +561,10 @@ function createAutocompleteTextWidgetFactory(
;(container as any).__widgetInputEl = widgetElementRef ;(container as any).__widgetInputEl = widgetElementRef
const metadataWidget = node.addWidget('text', metadataWidgetName, { const metadataWidget = node.addWidget('text', metadataWidgetName, {
version: 1, version: AUTOCOMPLETE_METADATA_VERSION,
textWidgetName: widgetName textWidgetName: widgetName
}) })
metadataWidget.value = createAutocompleteMetadataValue(widgetName)
metadataWidget.type = 'LORA_MANAGER_AUTOCOMPLETE_METADATA' metadataWidget.type = 'LORA_MANAGER_AUTOCOMPLETE_METADATA'
metadataWidget.hidden = true metadataWidget.hidden = true
metadataWidget.computeSize = () => [0, -4] metadataWidget.computeSize = () => [0, -4]
@@ -569,15 +702,38 @@ app.registerExtension({
// @ts-ignore // @ts-ignore
async beforeRegisterNodeDef(nodeType, nodeData) { async beforeRegisterNodeDef(nodeType, nodeData) {
const comfyClass = nodeType.comfyClass const comfyClass = nodeType.comfyClass
const inputs = { ...nodeData.input?.required, ...nodeData.input?.optional }
let hasAutocompleteWidget = false
// Extract and store input options for autocomplete widgets // Extract and store input options for autocomplete widgets
const inputs = { ...nodeData.input?.required, ...nodeData.input?.optional }
for (const [inputName, inputDef] of Object.entries(inputs)) { for (const [inputName, inputDef] of Object.entries(inputs)) {
// @ts-ignore // @ts-ignore
if (Array.isArray(inputDef) && typeof inputDef[0] === 'string' && inputDef[0].startsWith('AUTOCOMPLETE_TEXT_')) { if (Array.isArray(inputDef) && typeof inputDef[0] === 'string' && inputDef[0].startsWith('AUTOCOMPLETE_TEXT_')) {
// @ts-ignore // @ts-ignore
const options = inputDef[1] || {} const options = inputDef[1] || {}
widgetInputOptions.set(`${nodeData.name}:${inputName}`, options) widgetInputOptions.set(`${nodeData.name}:${inputName}`, options)
hasAutocompleteWidget = true
}
}
if (hasAutocompleteWidget) {
const originalOnSerialize = nodeType.prototype.onSerialize
const originalConfigure = nodeType.prototype.configure
nodeType.prototype.onSerialize = function (serialized: any) {
originalOnSerialize?.apply(this, arguments)
serialized.properties = serialized.properties || {}
const widgetIds = getSerializableWidgetNames(this)
serialized.properties[LORA_MANAGER_WIDGET_IDS_PROPERTY] = widgetIds
}
nodeType.prototype.configure = function (info: any) {
normalizeAutocompleteWidgetValues(this, info)
if (shouldBypassAutocompleteWidgetMigration(this, info?.widgets_values ?? [])) {
info.widgets_values = [...(info.widgets_values ?? []), null]
}
return originalConfigure?.apply(this, arguments)
} }
} }

View File

@@ -2118,14 +2118,14 @@ to { transform: rotate(360deg);
padding: 20px 0; padding: 20px 0;
} }
.autocomplete-text-widget[data-v-918e2bc5] { .autocomplete-text-widget[data-v-76ce0f19] {
background: transparent; background: transparent;
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
box-sizing: border-box; box-sizing: border-box;
} }
.input-wrapper[data-v-918e2bc5] { .input-wrapper[data-v-76ce0f19] {
position: relative; position: relative;
flex: 1; flex: 1;
display: flex; display: flex;
@@ -2133,7 +2133,7 @@ to { transform: rotate(360deg);
} }
/* Canvas mode styles (default) - matches built-in comfy-multiline-input */ /* Canvas mode styles (default) - matches built-in comfy-multiline-input */
.text-input[data-v-918e2bc5] { .text-input[data-v-76ce0f19] {
flex: 1; flex: 1;
width: 100%; width: 100%;
background-color: var(--comfy-input-bg, #222); background-color: var(--comfy-input-bg, #222);
@@ -2150,7 +2150,7 @@ to { transform: rotate(360deg);
} }
/* Vue DOM mode styles - matches built-in p-textarea in Vue DOM mode */ /* Vue DOM mode styles - matches built-in p-textarea in Vue DOM mode */
.text-input.vue-dom-mode[data-v-918e2bc5] { .text-input.vue-dom-mode[data-v-76ce0f19] {
background-color: var(--color-charcoal-400, #313235); background-color: var(--color-charcoal-400, #313235);
color: #fff; color: #fff;
padding: 8px 12px 30px 12px; /* Reserve bottom space for clear button */ padding: 8px 12px 30px 12px; /* Reserve bottom space for clear button */
@@ -2159,12 +2159,12 @@ to { transform: rotate(360deg);
font-size: 12px; font-size: 12px;
font-family: inherit; font-family: inherit;
} }
.text-input[data-v-918e2bc5]:focus { .text-input[data-v-76ce0f19]:focus {
outline: none; outline: none;
} }
/* Clear button styles */ /* Clear button styles */
.clear-button[data-v-918e2bc5] { .clear-button[data-v-76ce0f19] {
position: absolute; position: absolute;
right: 6px; right: 6px;
bottom: 6px; /* Changed from top to bottom */ bottom: 6px; /* Changed from top to bottom */
@@ -2187,31 +2187,31 @@ to { transform: rotate(360deg);
} }
/* Show clear button when hovering over input wrapper */ /* Show clear button when hovering over input wrapper */
.input-wrapper:hover .clear-button[data-v-918e2bc5] { .input-wrapper:hover .clear-button[data-v-76ce0f19] {
opacity: 0.7; opacity: 0.7;
pointer-events: auto; pointer-events: auto;
} }
.clear-button[data-v-918e2bc5]:hover { .clear-button[data-v-76ce0f19]:hover {
opacity: 1; opacity: 1;
background: rgba(255, 100, 100, 0.8); background: rgba(255, 100, 100, 0.8);
} }
.clear-button svg[data-v-918e2bc5] { .clear-button svg[data-v-76ce0f19] {
width: 12px; width: 12px;
height: 12px; height: 12px;
} }
/* Vue DOM mode adjustments for clear button */ /* Vue DOM mode adjustments for clear button */
.text-input.vue-dom-mode ~ .clear-button[data-v-918e2bc5] { .text-input.vue-dom-mode ~ .clear-button[data-v-76ce0f19] {
right: 8px; right: 8px;
bottom: 10px; /* Changed from top to bottom, adjusted for Vue DOM padding */ bottom: 10px; /* Changed from top to bottom, adjusted for Vue DOM padding */
width: 20px; width: 20px;
height: 20px; height: 20px;
background: rgba(107, 114, 128, 0.6); background: rgba(107, 114, 128, 0.6);
} }
.text-input.vue-dom-mode ~ .clear-button[data-v-918e2bc5]:hover { .text-input.vue-dom-mode ~ .clear-button[data-v-76ce0f19]:hover {
background: oklch(62% 0.18 25); background: oklch(62% 0.18 25);
} }
.text-input.vue-dom-mode ~ .clear-button svg[data-v-918e2bc5] { .text-input.vue-dom-mode ~ .clear-button svg[data-v-76ce0f19] {
width: 14px; width: 14px;
height: 14px; height: 14px;
}`)); }`));
@@ -14748,7 +14748,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
app2.canvas.processMouseWheel(event); app2.canvas.processMouseWheel(event);
} }
}; };
const onExternalValueChange = (event) => { const onExternalValueChange = () => {
updateHasTextState(); updateHasTextState();
}; };
const setupWidgetOnSetValue = () => { const setupWidgetOnSetValue = () => {
@@ -14847,7 +14847,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
}; };
} }
}); });
const AutocompleteTextWidget = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-918e2bc5"]]); const AutocompleteTextWidget = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-76ce0f19"]]);
const LORA_PROVIDER_NODE_TYPES$1 = [ const LORA_PROVIDER_NODE_TYPES$1 = [
"Lora Stacker (LoraManager)", "Lora Stacker (LoraManager)",
"Lora Randomizer (LoraManager)", "Lora Randomizer (LoraManager)",
@@ -15127,6 +15127,8 @@ const JSON_DISPLAY_WIDGET_MIN_WIDTH = 300;
const JSON_DISPLAY_WIDGET_MIN_HEIGHT = 200; const JSON_DISPLAY_WIDGET_MIN_HEIGHT = 200;
const AUTOCOMPLETE_TEXT_WIDGET_MIN_HEIGHT = 60; const AUTOCOMPLETE_TEXT_WIDGET_MIN_HEIGHT = 60;
const AUTOCOMPLETE_TEXT_WIDGET_MAX_HEIGHT = 100; const AUTOCOMPLETE_TEXT_WIDGET_MAX_HEIGHT = 100;
const AUTOCOMPLETE_METADATA_VERSION = 1;
const LORA_MANAGER_WIDGET_IDS_PROPERTY = "__lm_widget_ids";
function forwardMiddleMouseToCanvas(container) { function forwardMiddleMouseToCanvas(container) {
if (!container) return; if (!container) return;
container.addEventListener("pointerdown", (event) => { container.addEventListener("pointerdown", (event) => {
@@ -15397,6 +15399,97 @@ function createJsonDisplayWidget(node) {
return { widget }; return { widget };
} }
const widgetInputOptions = /* @__PURE__ */ new Map(); const widgetInputOptions = /* @__PURE__ */ new Map();
function getSerializableWidgetNames(node) {
return (node.widgets || []).filter((widget) => widget && widget.serialize !== false).map((widget) => widget.name);
}
function createAutocompleteMetadataValue(textWidgetName = "text") {
return {
version: AUTOCOMPLETE_METADATA_VERSION,
textWidgetName
};
}
function shouldBypassAutocompleteWidgetMigration(node, widgetValues) {
var _a2, _b;
const inputDefs = (_b = (_a2 = node == null ? void 0 : node.constructor) == null ? void 0 : _a2.nodeData) == null ? void 0 : _b.inputs;
if (!inputDefs || !Array.isArray(widgetValues)) {
return false;
}
const widgetNames = new Set((node.widgets || []).map((widget) => widget == null ? void 0 : widget.name));
const hasAutocompleteMetadataWidget = Array.from(widgetNames).some(
(name) => typeof name === "string" && name.startsWith("__lm_autocomplete_meta_")
);
if (!hasAutocompleteMetadataWidget) {
return false;
}
const originalWidgetsInputs = Object.values(inputDefs).filter(
(input) => widgetNames.has(input.name) || input.forceInput
);
const widgetIndexHasForceInput = originalWidgetsInputs.flatMap(
(input) => input.control_after_generate ? [!!input.forceInput, false] : [!!input.forceInput]
);
return widgetIndexHasForceInput.some(Boolean) && widgetIndexHasForceInput.length === widgetValues.length;
}
function remapWidgetValuesByName(widgetValues, savedWidgetNames, currentWidgetNames) {
const valueByName = /* @__PURE__ */ new Map();
savedWidgetNames.forEach((name, index) => {
if (index < widgetValues.length) {
valueByName.set(name, widgetValues[index]);
}
});
const remappedValues = [];
for (const name of currentWidgetNames) {
if (valueByName.has(name)) {
remappedValues.push(valueByName.get(name));
}
}
return remappedValues;
}
function injectDefaultAutocompleteMetadataValues(widgetValues, currentWidgetNames) {
const repairedValues = [];
let legacyValueIndex = 0;
for (const widgetName of currentWidgetNames) {
if (widgetName.startsWith("__lm_autocomplete_meta_")) {
const textWidgetName = widgetName.replace("__lm_autocomplete_meta_", "") || "text";
repairedValues.push(createAutocompleteMetadataValue(textWidgetName));
continue;
}
if (legacyValueIndex < widgetValues.length) {
repairedValues.push(widgetValues[legacyValueIndex]);
legacyValueIndex++;
}
}
return repairedValues;
}
function normalizeAutocompleteWidgetValues(node, info) {
var _a2;
if (!info || !Array.isArray(info.widgets_values)) {
return;
}
const currentWidgetNames = getSerializableWidgetNames(node);
if (currentWidgetNames.length === 0) {
return;
}
const savedWidgetNames = (_a2 = info.properties) == null ? void 0 : _a2[LORA_MANAGER_WIDGET_IDS_PROPERTY];
if (Array.isArray(savedWidgetNames) && savedWidgetNames.length > 0) {
const remappedValues = remapWidgetValuesByName(
info.widgets_values,
savedWidgetNames,
currentWidgetNames
);
info.widgets_values = remappedValues;
return;
}
const metadataWidgetCount = currentWidgetNames.filter(
(name) => name.startsWith("__lm_autocomplete_meta_")
).length;
if (metadataWidgetCount > 0 && info.widgets_values.length === currentWidgetNames.length - metadataWidgetCount) {
const repairedValues = injectDefaultAutocompleteMetadataValues(
info.widgets_values,
currentWidgetNames
);
info.widgets_values = repairedValues;
}
}
const initVueDomModeListener = () => { const initVueDomModeListener = () => {
var _a2, _b; var _a2, _b;
if ((_b = (_a2 = app$1.ui) == null ? void 0 : _a2.settings) == null ? void 0 : _b.addEventListener) { if ((_b = (_a2 = app$1.ui) == null ? void 0 : _a2.settings) == null ? void 0 : _b.addEventListener) {
@@ -15436,9 +15529,10 @@ function createAutocompleteTextWidgetFactory(node, widgetName, modelType, inputO
const widgetElementRef = { inputEl: void 0 }; const widgetElementRef = { inputEl: void 0 };
container.__widgetInputEl = widgetElementRef; container.__widgetInputEl = widgetElementRef;
const metadataWidget = node.addWidget("text", metadataWidgetName, { const metadataWidget = node.addWidget("text", metadataWidgetName, {
version: 1, version: AUTOCOMPLETE_METADATA_VERSION,
textWidgetName: widgetName textWidgetName: widgetName
}); });
metadataWidget.value = createAutocompleteMetadataValue(widgetName);
metadataWidget.type = "LORA_MANAGER_AUTOCOMPLETE_METADATA"; metadataWidget.type = "LORA_MANAGER_AUTOCOMPLETE_METADATA";
metadataWidget.hidden = true; metadataWidget.hidden = true;
metadataWidget.computeSize = () => [0, -4]; metadataWidget.computeSize = () => [0, -4];
@@ -15562,12 +15656,31 @@ app$1.registerExtension({
var _a2, _b; var _a2, _b;
const comfyClass = nodeType.comfyClass; const comfyClass = nodeType.comfyClass;
const inputs = { ...(_a2 = nodeData.input) == null ? void 0 : _a2.required, ...(_b = nodeData.input) == null ? void 0 : _b.optional }; const inputs = { ...(_a2 = nodeData.input) == null ? void 0 : _a2.required, ...(_b = nodeData.input) == null ? void 0 : _b.optional };
let hasAutocompleteWidget = false;
for (const [inputName, inputDef] of Object.entries(inputs)) { for (const [inputName, inputDef] of Object.entries(inputs)) {
if (Array.isArray(inputDef) && typeof inputDef[0] === "string" && inputDef[0].startsWith("AUTOCOMPLETE_TEXT_")) { if (Array.isArray(inputDef) && typeof inputDef[0] === "string" && inputDef[0].startsWith("AUTOCOMPLETE_TEXT_")) {
const options = inputDef[1] || {}; const options = inputDef[1] || {};
widgetInputOptions.set(`${nodeData.name}:${inputName}`, options); widgetInputOptions.set(`${nodeData.name}:${inputName}`, options);
hasAutocompleteWidget = true;
} }
} }
if (hasAutocompleteWidget) {
const originalOnSerialize = nodeType.prototype.onSerialize;
const originalConfigure = nodeType.prototype.configure;
nodeType.prototype.onSerialize = function(serialized) {
originalOnSerialize == null ? void 0 : originalOnSerialize.apply(this, arguments);
serialized.properties = serialized.properties || {};
const widgetIds = getSerializableWidgetNames(this);
serialized.properties[LORA_MANAGER_WIDGET_IDS_PROPERTY] = widgetIds;
};
nodeType.prototype.configure = function(info) {
normalizeAutocompleteWidgetValues(this, info);
if (shouldBypassAutocompleteWidgetMigration(this, (info == null ? void 0 : info.widgets_values) ?? [])) {
info.widgets_values = [...info.widgets_values ?? [], null];
}
return originalConfigure == null ? void 0 : originalConfigure.apply(this, arguments);
};
}
if (LORA_PROVIDER_NODE_TYPES$1.includes(comfyClass)) { if (LORA_PROVIDER_NODE_TYPES$1.includes(comfyClass)) {
const originalOnNodeCreated = nodeType.prototype.onNodeCreated; const originalOnNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function() { nodeType.prototype.onNodeCreated = function() {

File diff suppressed because one or more lines are too long