mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-08-23 05:51:26 -03:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7df83f44b8 | |||
| 169fa7bed6 | |||
| 027b504fe8 |
+16
-3
@@ -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 {
|
||||||
|
|||||||
@@ -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.
|
||||||
props.widget.options.getValue = buildValue
|
if (props.widget.options) {
|
||||||
props.widget.options.setValue = applyValue
|
props.widget.options.getValue = buildValue
|
||||||
|
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)
|
||||||
|
|||||||
@@ -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`) || {}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
props.widget.options.getValue = buildValue;
|
if (props.widget.options) {
|
||||||
props.widget.options.setValue = applyValue;
|
props.widget.options.getValue = buildValue;
|
||||||
|
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
Reference in New Issue
Block a user