Compare commits

..

3 Commits

8 changed files with 140 additions and 85 deletions
+16 -3
View File
@@ -252,6 +252,13 @@ class SaveImageLM:
"tooltip": "When enabled, embeds generation parameters into the saved image metadata. Disable to skip writing generation metadata.", "tooltip": "When enabled, embeds generation parameters into the saved image metadata. Disable to skip writing generation metadata.",
}, },
), ),
"add_loras_to_prompt": (
"BOOLEAN",
{
"default": False,
"tooltip": "When enabled, appends the LoRA syntax line (e.g. <lora:name:strength>) after the positive prompt in the saved metadata.",
},
),
"add_counter_to_filename": ( "add_counter_to_filename": (
"BOOLEAN", "BOOLEAN",
{ {
@@ -348,7 +355,7 @@ class SaveImageLM:
type_lower = model_type.lower() if model_type else "other" type_lower = model_type.lower() if model_type else "other"
return f"urn:air:{slug}:{type_lower}:civitai:{model_id}@{version_id}" return f"urn:air:{slug}:{type_lower}:civitai:{model_id}@{version_id}"
def format_metadata(self, metadata_dict: dict) -> str: def format_metadata(self, metadata_dict: dict, add_loras_to_prompt: bool = False) -> str:
"""Format metadata as A1111-compatible parameters string with Hashes JSON and Civitai resources.""" """Format metadata as A1111-compatible parameters string with Hashes JSON and Civitai resources."""
if not metadata_dict: return "" if not metadata_dict: return ""
@@ -458,7 +465,10 @@ class SaveImageLM:
scheduler_name = scheduler_mapping.get(scheduler, scheduler) if scheduler else None scheduler_name = scheduler_mapping.get(scheduler, scheduler) if scheduler else None
# Build output lines # Build output lines
lines = [prompt] if prompt else [""] prompt_line = prompt if prompt else ""
if add_loras_to_prompt and loras_text:
prompt_line = f"{prompt_line}\n{loras_text}" if prompt_line else loras_text
lines = [prompt_line] if prompt_line else [""]
if negative_prompt: if negative_prompt:
lines.append(f"Negative prompt: {negative_prompt}") lines.append(f"Negative prompt: {negative_prompt}")
@@ -793,6 +803,7 @@ class SaveImageLM:
save_with_metadata=True, save_with_metadata=True,
add_counter_to_filename=True, add_counter_to_filename=True,
save_as_recipe=False, save_as_recipe=False,
add_loras_to_prompt=False,
): ):
"""Save images with metadata""" """Save images with metadata"""
results = [] results = []
@@ -801,7 +812,7 @@ class SaveImageLM:
raw_metadata = get_metadata() raw_metadata = get_metadata()
metadata_dict = MetadataProcessor.to_dict(raw_metadata, id) metadata_dict = MetadataProcessor.to_dict(raw_metadata, id)
metadata = self.format_metadata(metadata_dict) metadata = self.format_metadata(metadata_dict, add_loras_to_prompt)
# Process filename_prefix with pattern substitution # Process filename_prefix with pattern substitution
filename_prefix = self.format_filename(filename_prefix, metadata_dict) filename_prefix = self.format_filename(filename_prefix, metadata_dict)
@@ -943,6 +954,7 @@ class SaveImageLM:
save_with_metadata=True, save_with_metadata=True,
add_counter_to_filename=True, add_counter_to_filename=True,
save_as_recipe=False, save_as_recipe=False,
add_loras_to_prompt=False,
): ):
"""Process and save image with metadata""" """Process and save image with metadata"""
# Make sure the output directory exists # Make sure the output directory exists
@@ -974,6 +986,7 @@ class SaveImageLM:
save_with_metadata, save_with_metadata,
add_counter_to_filename, add_counter_to_filename,
save_as_recipe, save_as_recipe,
add_loras_to_prompt,
) )
return { return {
+43
View File
@@ -86,6 +86,41 @@ def test_save_image_skips_png_parameters_when_metadata_disabled_and_keeps_workfl
assert img.info["workflow"] == json.dumps(workflow) assert img.info["workflow"] == json.dumps(workflow)
def test_save_image_does_not_append_loras_to_prompt_by_default(monkeypatch, tmp_path):
_configure_save_paths(monkeypatch, tmp_path)
_configure_metadata(
monkeypatch,
{"prompt": "prompt text", "seed": 123, "loras": "<lora:foo:0.7>"},
)
node = SaveImageLM()
node.save_images([_make_image()], "ComfyUI", "png", id="node-1")
image_path = tmp_path / "sample_00001_.png"
with Image.open(image_path) as img:
assert "<lora:" not in img.info["parameters"]
assert img.info["parameters"] == "prompt text\nSeed: 123, Version: ComfyUI"
def test_save_image_appends_loras_to_prompt_when_enabled(monkeypatch, tmp_path):
_configure_save_paths(monkeypatch, tmp_path)
_configure_metadata(
monkeypatch,
{"prompt": "prompt text", "seed": 123, "loras": "<lora:foo:0.7>"},
)
node = SaveImageLM()
node.save_images(
[_make_image()], "ComfyUI", "png", id="node-1", add_loras_to_prompt=True
)
image_path = tmp_path / "sample_00001_.png"
with Image.open(image_path) as img:
assert img.info["parameters"] == (
"prompt text\n<lora:foo:0.7>\nSeed: 123, Version: ComfyUI"
)
def test_save_image_skips_jpeg_metadata_when_disabled(monkeypatch, tmp_path): def test_save_image_skips_jpeg_metadata_when_disabled(monkeypatch, tmp_path):
_configure_save_paths(monkeypatch, tmp_path) _configure_save_paths(monkeypatch, tmp_path)
_configure_metadata(monkeypatch, {"prompt": "prompt text", "seed": 123}) _configure_metadata(monkeypatch, {"prompt": "prompt text", "seed": 123})
@@ -451,6 +486,14 @@ class TestParameterDefaultConsistency:
assert SaveImageLM.save_images.__defaults__[5] == 0 assert SaveImageLM.save_images.__defaults__[5] == 0
assert SaveImageLM.process_image.__defaults__[7] == 0 assert SaveImageLM.process_image.__defaults__[7] == 0
def test_add_loras_to_prompt_defaults_are_consistent(self):
input_types = SaveImageLM.INPUT_TYPES()
optional = input_types["optional"]
assert optional["add_loras_to_prompt"][1]["default"] is False
assert SaveImageLM.save_images.__defaults__[-1] is False
assert SaveImageLM.process_image.__defaults__[-1] is False
def test_png_does_not_pass_webp_method_or_jpeg_subsampling(monkeypatch, tmp_path): def test_png_does_not_pass_webp_method_or_jpeg_subsampling(monkeypatch, tmp_path):
_configure_save_paths(monkeypatch, tmp_path) _configure_save_paths(monkeypatch, tmp_path)
@@ -45,7 +45,7 @@ export interface AutocompleteTextWidgetInterface {
const props = defineProps<{ const props = defineProps<{
widget: AutocompleteTextWidgetInterface widget: AutocompleteTextWidgetInterface
node: { id: number } node: { id: number }
modelType?: 'loras' | 'embeddings' | 'custom_words' | 'prompt' modelType?: 'loras' | 'prompt'
placeholder?: string placeholder?: string
showPreview?: boolean showPreview?: boolean
spellcheck?: boolean spellcheck?: boolean
@@ -98,7 +98,7 @@ interface LoraInfoWidget {
onSetValue?: (v: unknown) => void onSetValue?: (v: unknown) => void
callback?: unknown callback?: unknown
options?: { options?: {
getValue?: () => LoraInfoWidgetValue getValue?: () => unknown
setValue?: (v: unknown) => void setValue?: (v: unknown) => void
} }
node?: { widgets?: Array<{ id?: string }>; widgets_values?: Array<unknown> } node?: { widgets?: Array<{ id?: string }>; widgets_values?: Array<unknown> }
@@ -299,8 +299,12 @@ onMounted(() => {
// ComponentWidgetImpl.value getter/setter delegates to options.getValue/options.setValue. // ComponentWidgetImpl.value getter/setter delegates to options.getValue/options.setValue.
// These must be set for workflow JSON persistence (LGraphNode.serialize/configure) to work. // These must be set for workflow JSON persistence (LGraphNode.serialize/configure) to work.
if (props.widget.options) {
props.widget.options.getValue = buildValue props.widget.options.getValue = buildValue
props.widget.options.setValue = applyValue props.widget.options.setValue = applyValue
} else {
console.warn('[LoraInfoWidget] widget.options missing, value persistence disabled')
}
// Also set serializeValue for prompt/API serialization path (executionUtil.ts) // Also set serializeValue for prompt/API serialization path (executionUtil.ts)
props.widget.serializeValue = async () => buildValue() props.widget.serializeValue = async () => buildValue()
@@ -3,7 +3,7 @@ import { ref, onMounted, onUnmounted, type Ref } from 'vue'
// Dynamic import type for AutoComplete class // Dynamic import type for AutoComplete class
type AutoCompleteClass = new ( type AutoCompleteClass = new (
inputElement: HTMLTextAreaElement, inputElement: HTMLTextAreaElement,
modelType: 'loras' | 'embeddings' | 'custom_words' | 'prompt', modelType: 'loras' | 'prompt',
options?: AutocompleteOptions options?: AutocompleteOptions
) => AutoCompleteInstance ) => AutoCompleteInstance
@@ -29,7 +29,7 @@ export interface UseAutocompleteOptions {
export function useAutocomplete( export function useAutocomplete(
textareaRef: Ref<HTMLTextAreaElement | null>, textareaRef: Ref<HTMLTextAreaElement | null>,
modelType: 'loras' | 'embeddings' | 'custom_words' | 'prompt' = 'loras', modelType: 'loras' | 'prompt' = 'loras',
options: UseAutocompleteOptions = {} options: UseAutocompleteOptions = {}
) { ) {
const autocompleteInstance = ref<AutoCompleteInstance | null>(null) const autocompleteInstance = ref<AutoCompleteInstance | null>(null)
+6 -9
View File
@@ -36,6 +36,9 @@ const AUTOCOMPLETE_TEXT_MIN_HEIGHT_DEFAULT = 300
const AUTOCOMPLETE_METADATA_VERSION = 1 const AUTOCOMPLETE_METADATA_VERSION = 1
const LORA_MANAGER_WIDGET_IDS_PROPERTY = '__lm_widget_ids' const LORA_MANAGER_WIDGET_IDS_PROPERTY = '__lm_widget_ids'
// Access LiteGraph global for Vue DOM mode detection (matches AutocompleteTextWidget.vue)
declare const LiteGraph: { vueNodesMode?: boolean } | undefined
// @ts-ignore - ComfyUI external module // @ts-ignore - ComfyUI external module
import { app } from '../../../scripts/app.js' import { app } from '../../../scripts/app.js'
// @ts-ignore - ComfyUI external module // @ts-ignore - ComfyUI external module
@@ -718,7 +721,7 @@ function createLoraInfoWidget(node: any) {
function createAutocompleteTextWidgetFactory( function createAutocompleteTextWidgetFactory(
node: any, node: any,
widgetName: string, widgetName: string,
modelType: 'loras' | 'embeddings' | 'prompt', modelType: 'loras' | 'prompt',
inputOptions: { placeholder?: string } = {} inputOptions: { placeholder?: string } = {}
) { ) {
const metadataWidgetName = `__lm_autocomplete_meta_${widgetName}` const metadataWidgetName = `__lm_autocomplete_meta_${widgetName}`
@@ -835,7 +838,7 @@ function createAutocompleteTextWidgetFactory(
applyAutocompleteTextLayoutFix( applyAutocompleteTextLayoutFix(
widget, widget,
container, container,
typeof LiteGraph !== 'undefined' && LiteGraph.vueNodesMode typeof LiteGraph !== 'undefined' && LiteGraph.vueNodesMode === true
) )
} }
@@ -964,13 +967,7 @@ app.registerExtension({
const options = widgetInputOptions.get(`${node.comfyClass}:text`) || {} const options = widgetInputOptions.get(`${node.comfyClass}:text`) || {}
return createAutocompleteTextWidgetFactory(node, 'text', 'loras', options) return createAutocompleteTextWidgetFactory(node, 'text', 'loras', options)
}, },
// Autocomplete text widget for embeddings (used by Prompt node) // Autocomplete text widget for prompt (used by Prompt and Text nodes)
// @ts-ignore
AUTOCOMPLETE_TEXT_EMBEDDINGS(node) {
const options = widgetInputOptions.get(`${node.comfyClass}:text`) || {}
return createAutocompleteTextWidgetFactory(node, 'text', 'embeddings', options)
},
// Autocomplete text widget for prompt (supports both embeddings and custom words)
// @ts-ignore // @ts-ignore
AUTOCOMPLETE_TEXT_PROMPT(node) { AUTOCOMPLETE_TEXT_PROMPT(node) {
const options = widgetInputOptions.get(`${node.comfyClass}:text`) || {} const options = widgetInputOptions.get(`${node.comfyClass}:text`) || {}
+62 -64
View File
@@ -2118,14 +2118,14 @@ to { transform: rotate(360deg);
padding: 20px 0; padding: 20px 0;
} }
.autocomplete-text-widget[data-v-3f3d7a1a] { .autocomplete-text-widget[data-v-55e3316e] {
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-3f3d7a1a] { .input-wrapper[data-v-55e3316e] {
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-3f3d7a1a] { .text-input[data-v-55e3316e] {
flex: 1; flex: 1;
width: 100%; width: 100%;
background-color: var(--comfy-input-bg, #222); background-color: var(--comfy-input-bg, #222);
@@ -2152,7 +2152,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-3f3d7a1a] { .text-input.vue-dom-mode[data-v-55e3316e] {
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 */
@@ -2161,12 +2161,12 @@ to { transform: rotate(360deg);
font-size: 12px; font-size: 12px;
font-family: inherit; font-family: inherit;
} }
.text-input[data-v-3f3d7a1a]:focus { .text-input[data-v-55e3316e]:focus {
outline: none; outline: none;
} }
/* Clear button styles */ /* Clear button styles */
.clear-button[data-v-3f3d7a1a] { .clear-button[data-v-55e3316e] {
position: absolute; position: absolute;
right: 6px; right: 6px;
bottom: 6px; /* Changed from top to bottom */ bottom: 6px; /* Changed from top to bottom */
@@ -2189,31 +2189,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-3f3d7a1a] { .input-wrapper:hover .clear-button[data-v-55e3316e] {
opacity: 0.7; opacity: 0.7;
pointer-events: auto; pointer-events: auto;
} }
.clear-button[data-v-3f3d7a1a]:hover { .clear-button[data-v-55e3316e]:hover {
opacity: 1; opacity: 1;
background: rgba(255, 100, 100, 0.8); background: rgba(255, 100, 100, 0.8);
} }
.clear-button svg[data-v-3f3d7a1a] { .clear-button svg[data-v-55e3316e] {
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-3f3d7a1a] { .text-input.vue-dom-mode ~ .clear-button[data-v-55e3316e] {
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-3f3d7a1a]:hover { .text-input.vue-dom-mode ~ .clear-button[data-v-55e3316e]:hover {
background: oklch(62% 0.18 25); background: oklch(62% 0.18 25);
} }
.text-input.vue-dom-mode ~ .clear-button svg[data-v-3f3d7a1a] { .text-input.vue-dom-mode ~ .clear-button svg[data-v-55e3316e] {
width: 14px; width: 14px;
height: 14px; height: 14px;
} }
@@ -2224,7 +2224,7 @@ to { transform: rotate(360deg);
resize: vertical !important; resize: vertical !important;
} }
.lora-info-widget[data-v-a99cc1ab] { .lora-info-widget[data-v-d7692b6f] {
padding: 12px; padding: 12px;
background: rgba(40, 44, 52, 0.6); background: rgba(40, 44, 52, 0.6);
border-radius: 4px; border-radius: 4px;
@@ -2240,45 +2240,45 @@ to { transform: rotate(360deg);
determined solely by CSS not by descendant content. This breaks the determined solely by CSS not by descendant content. This breaks the
feedback loop where content grows ResizeObserver resizes content feedback loop where content grows ResizeObserver resizes content
reflows repeat. Same technique used by tags_widget.js + lm_styles.css. */ reflows repeat. Same technique used by tags_widget.js + lm_styles.css. */
.lora-info-widget.lm-vue-node[data-v-a99cc1ab] { .lora-info-widget.lm-vue-node[data-v-d7692b6f] {
contain: layout size; contain: layout size;
} }
/* ── Tab bar ── */ /* ── Tab bar ── */
.lora-info-tabs[data-v-a99cc1ab] { .lora-info-tabs[data-v-d7692b6f] {
display: flex; display: flex;
gap: 0; gap: 0;
margin-bottom: 10px; margin-bottom: 10px;
border-bottom: 1px solid var(--border-color, #444); border-bottom: 1px solid var(--border-color, #444);
flex-shrink: 0; flex-shrink: 0;
} }
.lora-info-tab[data-v-a99cc1ab] { .lora-info-tab[data-v-d7692b6f] {
flex: 1; flex: 1;
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;
padding: 6px 0; padding: 6px 0;
position: relative; position: relative;
} }
.lora-info-tab-input[data-v-a99cc1ab] { .lora-info-tab-input[data-v-d7692b6f] {
position: absolute; position: absolute;
opacity: 0; opacity: 0;
width: 0; width: 0;
height: 0; height: 0;
} }
.lora-info-tab-label[data-v-a99cc1ab] { .lora-info-tab-label[data-v-d7692b6f] {
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 500;
color: var(--fg-color, #fff); color: var(--fg-color, #fff);
opacity: 0.5; opacity: 0.5;
transition: opacity 0.15s; transition: opacity 0.15s;
} }
.lora-info-tab:hover .lora-info-tab-label[data-v-a99cc1ab] { .lora-info-tab:hover .lora-info-tab-label[data-v-d7692b6f] {
opacity: 0.75; opacity: 0.75;
} }
.lora-info-tab.active .lora-info-tab-label[data-v-a99cc1ab] { .lora-info-tab.active .lora-info-tab-label[data-v-d7692b6f] {
opacity: 1; opacity: 1;
} }
.lora-info-tab.active[data-v-a99cc1ab]::after { .lora-info-tab.active[data-v-d7692b6f]::after {
content: ''; content: '';
position: absolute; position: absolute;
bottom: -1px; bottom: -1px;
@@ -2290,16 +2290,16 @@ to { transform: rotate(360deg);
} }
/* ── Tab content ── */ /* ── Tab content ── */
.tab-content[data-v-a99cc1ab] { .tab-content[data-v-d7692b6f] {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
overflow: hidden; overflow: hidden;
} }
.notes-tab[data-v-a99cc1ab] { .notes-tab[data-v-d7692b6f] {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.description-tab[data-v-a99cc1ab] { .description-tab[data-v-d7692b6f] {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow-y: auto; overflow-y: auto;
@@ -2307,12 +2307,12 @@ to { transform: rotate(360deg);
} }
/* ── Info fields (shared) ── */ /* ── Info fields (shared) ── */
.info-field[data-v-a99cc1ab] { .info-field[data-v-d7692b6f] {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 4px; gap: 4px;
} }
.info-label[data-v-a99cc1ab] { .info-label[data-v-d7692b6f] {
font-size: 10px; font-size: 10px;
font-weight: 600; font-weight: 600;
text-transform: uppercase; text-transform: uppercase;
@@ -2320,7 +2320,7 @@ to { transform: rotate(360deg);
color: var(--fg-color, #fff); color: var(--fg-color, #fff);
opacity: 0.6; opacity: 0.6;
} }
.lora-filename[data-v-a99cc1ab] { .lora-filename[data-v-d7692b6f] {
font-size: 13px; font-size: 13px;
font-weight: 500; font-weight: 500;
color: var(--fg-color, #fff); color: var(--fg-color, #fff);
@@ -2331,11 +2331,11 @@ to { transform: rotate(360deg);
user-select: text; user-select: text;
-webkit-user-select: text; -webkit-user-select: text;
} }
.notes-field[data-v-a99cc1ab] { .notes-field[data-v-d7692b6f] {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
} }
.lora-notes[data-v-a99cc1ab] { .lora-notes[data-v-d7692b6f] {
width: 100%; width: 100%;
flex: 1; flex: 1;
min-height: 60px; min-height: 60px;
@@ -2350,14 +2350,14 @@ to { transform: rotate(360deg);
font-family: inherit; font-family: inherit;
outline: none; outline: none;
} }
.lora-notes[data-v-a99cc1ab]:focus { .lora-notes[data-v-d7692b6f]:focus {
border-color: var(--comfy-input-border, #444); border-color: var(--comfy-input-border, #444);
} }
.lora-notes[data-v-a99cc1ab]:disabled { .lora-notes[data-v-d7692b6f]:disabled {
opacity: 0.6; opacity: 0.6;
cursor: not-allowed; cursor: not-allowed;
} }
.save-btn[data-v-a99cc1ab] { .save-btn[data-v-d7692b6f] {
width: 100%; width: 100%;
margin-top: 8px; margin-top: 8px;
padding: 6px 12px; padding: 6px 12px;
@@ -2371,11 +2371,11 @@ to { transform: rotate(360deg);
box-sizing: border-box; box-sizing: border-box;
flex-shrink: 0; flex-shrink: 0;
} }
.save-btn[data-v-a99cc1ab]:hover:not(:disabled) { .save-btn[data-v-d7692b6f]:hover:not(:disabled) {
background: rgba(66, 153, 225, 0.25); background: rgba(66, 153, 225, 0.25);
border-color: rgba(66, 153, 225, 0.6); border-color: rgba(66, 153, 225, 0.6);
} }
.save-btn[data-v-a99cc1ab]:disabled { .save-btn[data-v-d7692b6f]:disabled {
opacity: 0.4; opacity: 0.4;
cursor: not-allowed; cursor: not-allowed;
background: rgba(66, 153, 225, 0.05); background: rgba(66, 153, 225, 0.05);
@@ -2383,7 +2383,7 @@ to { transform: rotate(360deg);
} }
/* ── Description states ── */ /* ── Description states ── */
.description-state[data-v-a99cc1ab] { .description-state[data-v-d7692b6f] {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -2395,22 +2395,22 @@ to { transform: rotate(360deg);
min-height: 0; min-height: 0;
flex-shrink: 0; flex-shrink: 0;
} }
.description-state.error[data-v-a99cc1ab] { .description-state.error[data-v-d7692b6f] {
opacity: 0.7; opacity: 0.7;
color: #f87171; color: #f87171;
} }
/* ── Description content ── */ /* ── Description content ── */
.description-content[data-v-a99cc1ab] { .description-content[data-v-d7692b6f] {
min-height: 0; min-height: 0;
} }
.description-section[data-v-a99cc1ab] { .description-section[data-v-d7692b6f] {
margin-bottom: 14px; margin-bottom: 14px;
} }
.description-section[data-v-a99cc1ab]:last-child { .description-section[data-v-d7692b6f]:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
.description-text[data-v-a99cc1ab] { .description-text[data-v-d7692b6f] {
padding: 8px 0; padding: 8px 0;
font-size: 12px; font-size: 12px;
line-height: 1.5; line-height: 1.5;
@@ -2422,41 +2422,41 @@ to { transform: rotate(360deg);
user-select: text; user-select: text;
-webkit-user-select: text; -webkit-user-select: text;
} }
.description-text[data-v-a99cc1ab] p { .description-text[data-v-d7692b6f] p {
margin: 0 0 8px 0; margin: 0 0 8px 0;
} }
.description-text[data-v-a99cc1ab] p:last-child { .description-text[data-v-d7692b6f] p:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
.description-text[data-v-a99cc1ab] a { .description-text[data-v-d7692b6f] a {
color: rgba(66, 153, 225, 0.9); color: rgba(66, 153, 225, 0.9);
} }
.description-text[data-v-a99cc1ab] ul, .description-text[data-v-d7692b6f] ul,
.description-text[data-v-a99cc1ab] ol { .description-text[data-v-d7692b6f] ol {
padding-left: 20px; padding-left: 20px;
margin: 4px 0; margin: 4px 0;
} }
.description-text[data-v-a99cc1ab] h1, .description-text[data-v-d7692b6f] h1,
.description-text[data-v-a99cc1ab] h2, .description-text[data-v-d7692b6f] h2,
.description-text[data-v-a99cc1ab] h3 { .description-text[data-v-d7692b6f] h3 {
font-size: 13px; font-size: 13px;
margin: 10px 0 4px 0; margin: 10px 0 4px 0;
font-weight: 600; font-weight: 600;
opacity: 0.95; opacity: 0.95;
} }
.description-text[data-v-a99cc1ab] code { .description-text[data-v-d7692b6f] code {
background: rgba(255, 255, 255, 0.08); background: rgba(255, 255, 255, 0.08);
padding: 1px 4px; padding: 1px 4px;
border-radius: 3px; border-radius: 3px;
font-size: 11px; font-size: 11px;
} }
.description-text[data-v-a99cc1ab] img { .description-text[data-v-d7692b6f] img {
max-width: 100%; max-width: 100%;
border-radius: 4px; border-radius: 4px;
} }
/* ── Placeholder (shared) ── */ /* ── Placeholder (shared) ── */
.placeholder[data-v-a99cc1ab] { .placeholder[data-v-d7692b6f] {
font-style: italic; font-style: italic;
color: rgba(226, 232, 240, 0.5); color: rgba(226, 232, 240, 0.5);
text-align: center; text-align: center;
@@ -2465,10 +2465,10 @@ to { transform: rotate(360deg);
} }
/* ── Spinner (Font Awesome) ── */ /* ── Spinner (Font Awesome) ── */
.fa-spinner[data-v-a99cc1ab] { .fa-spinner[data-v-d7692b6f] {
animation: fa-spin-a99cc1ab 1s linear infinite; animation: fa-spin-d7692b6f 1s linear infinite;
} }
@keyframes fa-spin-a99cc1ab { @keyframes fa-spin-d7692b6f {
0% { transform: rotate(0deg); 0% { transform: rotate(0deg);
} }
100% { transform: rotate(360deg); 100% { transform: rotate(360deg);
@@ -15316,7 +15316,7 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
}; };
} }
}); });
const AutocompleteTextWidget = /* @__PURE__ */ _export_sfc(_sfc_main$1, [["__scopeId", "data-v-3f3d7a1a"]]); const AutocompleteTextWidget = /* @__PURE__ */ _export_sfc(_sfc_main$1, [["__scopeId", "data-v-55e3316e"]]);
const _hoisted_1 = { class: "lora-info-tabs" }; const _hoisted_1 = { class: "lora-info-tabs" };
const _hoisted_2 = { class: "tab-content notes-tab" }; const _hoisted_2 = { class: "tab-content notes-tab" };
const _hoisted_3 = { class: "info-field" }; const _hoisted_3 = { class: "info-field" };
@@ -15511,8 +15511,12 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
if (data.filePath !== void 0) filePath.value = data.filePath; if (data.filePath !== void 0) filePath.value = data.filePath;
} }
}; };
if (props.widget.options) {
props.widget.options.getValue = buildValue; props.widget.options.getValue = buildValue;
props.widget.options.setValue = applyValue; props.widget.options.setValue = applyValue;
} else {
console.warn("[LoraInfoWidget] widget.options missing, value persistence disabled");
}
props.widget.serializeValue = async () => buildValue(); props.widget.serializeValue = async () => buildValue();
props.widget.onSetValue = applyValue; props.widget.onSetValue = applyValue;
const widgetIndex = (_b = (_a2 = props.widget.node) == null ? void 0 : _a2.widgets) == null ? void 0 : _b.findIndex( const widgetIndex = (_b = (_a2 = props.widget.node) == null ? void 0 : _a2.widgets) == null ? void 0 : _b.findIndex(
@@ -15641,7 +15645,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
}; };
} }
}); });
const LoraInfoWidget = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-a99cc1ab"]]); const LoraInfoWidget = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-d7692b6f"]]);
function createVueWidgetCleanup(vueApp, onCleanup) { function createVueWidgetCleanup(vueApp, onCleanup) {
let didUnmount = false; let didUnmount = false;
return () => { return () => {
@@ -16637,7 +16641,7 @@ function createAutocompleteTextWidgetFactory(node, widgetName, modelType, inputO
applyAutocompleteTextLayoutFix( applyAutocompleteTextLayoutFix(
widget, widget,
container, container,
typeof LiteGraph !== "undefined" && LiteGraph.vueNodesMode typeof LiteGraph !== "undefined" && LiteGraph.vueNodesMode === true
); );
} }
const vueCleanup = createVueWidgetCleanup(vueApp, () => { const vueCleanup = createVueWidgetCleanup(vueApp, () => {
@@ -16747,13 +16751,7 @@ app$1.registerExtension({
const options = widgetInputOptions.get(`${node.comfyClass}:text`) || {}; const options = widgetInputOptions.get(`${node.comfyClass}:text`) || {};
return createAutocompleteTextWidgetFactory(node, "text", "loras", options); return createAutocompleteTextWidgetFactory(node, "text", "loras", options);
}, },
// Autocomplete text widget for embeddings (used by Prompt node) // Autocomplete text widget for prompt (used by Prompt and Text nodes)
// @ts-ignore
AUTOCOMPLETE_TEXT_EMBEDDINGS(node) {
const options = widgetInputOptions.get(`${node.comfyClass}:text`) || {};
return createAutocompleteTextWidgetFactory(node, "text", "embeddings", options);
},
// Autocomplete text widget for prompt (supports both embeddings and custom words)
// @ts-ignore // @ts-ignore
AUTOCOMPLETE_TEXT_PROMPT(node) { AUTOCOMPLETE_TEXT_PROMPT(node) {
const options = widgetInputOptions.get(`${node.comfyClass}:text`) || {}; const options = widgetInputOptions.get(`${node.comfyClass}:text`) || {};
File diff suppressed because one or more lines are too long