mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-29 17:08:52 -03:00
fix(nodes): preserve autocomplete widget values across workflow restore
This commit is contained in:
@@ -139,7 +139,7 @@ const onWheel = (event: WheelEvent) => {
|
||||
}
|
||||
|
||||
// Handle external value changes (e.g., from "send lora to workflow")
|
||||
const onExternalValueChange = (event: CustomEvent<{ value: string }>) => {
|
||||
const onExternalValueChange = () => {
|
||||
updateHasTextState()
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ const JSON_DISPLAY_WIDGET_MIN_WIDTH = 300
|
||||
const JSON_DISPLAY_WIDGET_MIN_HEIGHT = 200
|
||||
const AUTOCOMPLETE_TEXT_WIDGET_MIN_HEIGHT = 60
|
||||
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
|
||||
import { app } from '../../../scripts/app.js'
|
||||
@@ -373,6 +375,136 @@ function createJsonDisplayWidget(node) {
|
||||
// Store nodeData options per widget type for autocomplete widgets
|
||||
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
|
||||
const initVueDomModeListener = () => {
|
||||
if (app.ui?.settings?.addEventListener) {
|
||||
@@ -429,9 +561,10 @@ function createAutocompleteTextWidgetFactory(
|
||||
;(container as any).__widgetInputEl = widgetElementRef
|
||||
|
||||
const metadataWidget = node.addWidget('text', metadataWidgetName, {
|
||||
version: 1,
|
||||
version: AUTOCOMPLETE_METADATA_VERSION,
|
||||
textWidgetName: widgetName
|
||||
})
|
||||
metadataWidget.value = createAutocompleteMetadataValue(widgetName)
|
||||
metadataWidget.type = 'LORA_MANAGER_AUTOCOMPLETE_METADATA'
|
||||
metadataWidget.hidden = true
|
||||
metadataWidget.computeSize = () => [0, -4]
|
||||
@@ -569,15 +702,38 @@ app.registerExtension({
|
||||
// @ts-ignore
|
||||
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||
const comfyClass = nodeType.comfyClass
|
||||
const inputs = { ...nodeData.input?.required, ...nodeData.input?.optional }
|
||||
let hasAutocompleteWidget = false
|
||||
|
||||
// Extract and store input options for autocomplete widgets
|
||||
const inputs = { ...nodeData.input?.required, ...nodeData.input?.optional }
|
||||
for (const [inputName, inputDef] of Object.entries(inputs)) {
|
||||
// @ts-ignore
|
||||
if (Array.isArray(inputDef) && typeof inputDef[0] === 'string' && inputDef[0].startsWith('AUTOCOMPLETE_TEXT_')) {
|
||||
// @ts-ignore
|
||||
const options = inputDef[1] || {}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user