Compare commits

..
Author SHA1 Message Date
Will Miao ed7cf418b4 fix(autocomplete): never show a stale /activefilters Tip when the toggle is on
The one-time dropdown Tip ('type /activefilters ...') was inserted while the
active-filters search was OFF and then stuck around: _maybeShowFirstRunHint
bailed out while a hint element existed, so toggling the search ON from the
node's filter chip (which can keep the dropdown open in ComfyUI) left the
enable-hint visible even though the feature was now active.

- _maybeShowFirstRunHint now removes any existing hint and recomputes from the
  live setting on every dropdown render, so a Tip is never kept for the
  previous state.
- AutoComplete listens for the lora-manager:setting-toggled window event and
  refreshes an open dropdown's state hints (Tip in plain suggestions, footer
  text in the slash command list) immediately when the toggle changes, instead
  of waiting for the next hide/show cycle.

Regression tests cover: stale Tip removed when toggled ON while the dropdown
stays open (and not resurrected by further typing), and the command-list
footer flipping to ON.
2026-09-04 12:41:44 +08:00
9 changed files with 668 additions and 233 deletions
@@ -253,4 +253,126 @@ describe('AutoComplete active-filters flag', () => {
expect(autoComplete.dropdown.querySelector('.lm-autocomplete-first-run-hint')).toBeNull(); expect(autoComplete.dropdown.querySelector('.lm-autocomplete-first-run-hint')).toBeNull();
}); });
it('broadcasts a setting-toggled window event when /activefilters is accepted', async () => {
const events = [];
const listener = (event) => events.push(event.detail);
window.addEventListener('lora-manager:setting-toggled', listener);
try {
const input = document.createElement('textarea');
input.value = '/activefilters';
input.selectionStart = input.value.length;
input.focus = vi.fn();
input.setSelectionRange = vi.fn();
document.body.append(input);
caretHelperInstance.getBeforeCursor.mockReturnValue('/activefilters');
const { AutoComplete } = await import(AUTOCOMPLETE_MODULE);
const autoComplete = new AutoComplete(input, 'loras', { showPreview: false, minChars: 1 });
input.dispatchEvent(new Event('input', { bubbles: true }));
// The command token is cleared after acceptance; simulate the caret
// helper seeing the cleared input so the synthetic input event does
// not re-trigger command parsing (same pattern as behavior tests).
caretHelperInstance.getBeforeCursor.mockReturnValue('');
await Promise.resolve();
await Promise.resolve();
expect(events).toContainEqual({
settingId: 'loramanager.lora_active_filters_autocomplete',
value: true,
});
} finally {
window.removeEventListener('lora-manager:setting-toggled', listener);
}
});
it('removes a stale first-run hint when the toggle is switched on while the dropdown stays open', async () => {
localStorage.removeItem('lm:activefilters-tip-dismissed');
let enabled = false;
settingGetMock.mockImplementation((key) => {
if (key === 'loramanager.lora_active_filters_autocomplete') return enabled;
if (key === 'loramanager.autocomplete_append_comma') return false;
if (key === 'loramanager.autocomplete_auto_format') return false;
if (key === 'loramanager.autocomplete_accept_key') return 'both';
return undefined;
});
fetchApiMock.mockResolvedValue({
json: () => Promise.resolve({ success: true, relative_paths: ['models/example.safetensors'] }),
});
const input = document.createElement('textarea');
input.value = 'example';
input.selectionStart = 7;
document.body.append(input);
caretHelperInstance.getBeforeCursor.mockReturnValue('example');
const { AutoComplete } = await import(AUTOCOMPLETE_MODULE);
const autoComplete = new AutoComplete(input, 'loras', {
debounceDelay: 0,
showPreview: false,
minChars: 1,
});
const triggerShow = async () => {
input.dispatchEvent(new Event('input', { bubbles: true }));
await vi.runOnlyPendingTimersAsync();
await vi.runOnlyPendingTimersAsync();
await Promise.resolve();
return autoComplete.dropdown.querySelector('.lm-autocomplete-first-run-hint');
};
// OFF → the enable hint is shown in the suggestions dropdown.
expect(await triggerShow()).not.toBeNull();
// The node's filter chip toggles the setting ON while the dropdown is
// still open (ComfyUI can keep focus in the textarea, so no blur/hide
// fires). settings.js broadcasts the setting-toggled event.
enabled = true;
window.dispatchEvent(new CustomEvent('lora-manager:setting-toggled', {
detail: { settingId: 'loramanager.lora_active_filters_autocomplete', value: true },
}));
// The stale OFF hint must be gone even though the dropdown never closed.
expect(autoComplete.dropdown.querySelector('.lm-autocomplete-first-run-hint')).toBeNull();
// Further typing while ON must not resurrect the enable hint.
expect(await triggerShow()).toBeNull();
});
it('updates the command-list footer when the toggle changes while the command list is open', async () => {
let enabled = false;
settingGetMock.mockImplementation((key) => {
if (key === 'loramanager.lora_active_filters_autocomplete') return enabled;
return undefined;
});
const input = document.createElement('textarea');
input.value = '/';
input.selectionStart = 1;
document.body.append(input);
caretHelperInstance.getBeforeCursor.mockReturnValue('/');
const { AutoComplete } = await import(AUTOCOMPLETE_MODULE);
const autoComplete = new AutoComplete(input, 'loras', { showPreview: false, minChars: 1 });
input.dispatchEvent(new Event('input', { bubbles: true }));
await vi.runOnlyPendingTimersAsync();
await Promise.resolve();
const footer = () => autoComplete.dropdown.querySelector('.lm-autocomplete-command-footer');
expect(footer()).not.toBeNull();
expect(footer().textContent).toContain('Active Filters Search: OFF');
expect(footer().textContent).toContain('/activefilters to enable');
enabled = true;
window.dispatchEvent(new CustomEvent('lora-manager:setting-toggled', {
detail: { settingId: 'loramanager.lora_active_filters_autocomplete', value: true },
}));
expect(footer()).not.toBeNull();
expect(footer().textContent).toContain('Active Filters Search: ON');
expect(footer().textContent).toContain('/noactivefilters to disable');
});
}); });
@@ -54,6 +54,7 @@ const setSettingValueMock = vi.fn();
vi.mock(SETTINGS_MODULE, () => ({ vi.mock(SETTINGS_MODULE, () => ({
LORA_ACTIVE_FILTERS_AUTOCOMPLETE_SETTING_ID: LORA_ACTIVE_FILTERS_AUTOCOMPLETE_SETTING_ID:
"loramanager.lora_active_filters_autocomplete", "loramanager.lora_active_filters_autocomplete",
SETTING_TOGGLED_EVENT_NAME: "lora-manager:setting-toggled",
getLoraActiveFiltersAutocompletePreference: getActiveFiltersPreferenceMock, getLoraActiveFiltersAutocompletePreference: getActiveFiltersPreferenceMock,
setLoraManagerSettingValue: setSettingValueMock, setLoraManagerSettingValue: setSettingValueMock,
})); }));
@@ -27,6 +27,18 @@
<line x1="6" y1="6" x2="18" y2="18" /> <line x1="6" y1="6" x2="18" y2="18" />
</svg> </svg>
</button> </button>
<button
v-if="isLorasMode"
type="button"
class="active-filters-toggle"
:class="{ 'is-active': activeFiltersEnabled }"
:title="activeFiltersToggleTitle"
@click="toggleActiveFiltersSearch"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" />
</svg>
</button>
</div> </div>
</div> </div>
</template> </template>
@@ -34,6 +46,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue' import { ref, onMounted, onUnmounted, computed } from 'vue'
import { useAutocomplete } from '@/composables/useAutocomplete' import { useAutocomplete } from '@/composables/useAutocomplete'
// @ts-ignore - ComfyUI external module
import { LORA_ACTIVE_FILTERS_AUTOCOMPLETE_SETTING_ID, SETTING_TOGGLED_EVENT_NAME, getLoraActiveFiltersAutocompletePreference, setLoraManagerSettingValue } from '../../../web/comfyui/settings.js'
// Access LiteGraph global for initial mode detection // Access LiteGraph global for initial mode detection
declare const LiteGraph: { vueNodesMode?: boolean } | undefined declare const LiteGraph: { vueNodesMode?: boolean } | undefined
@@ -73,10 +87,10 @@ const inputWrapperRef = ref<HTMLElement | null>(null)
// Width of the textarea's own vertical scrollbar gutter. When the content // Width of the textarea's own vertical scrollbar gutter. When the content
// overflows and a classic (non-overlay) scrollbar is shown, the scrollbar // overflows and a classic (non-overlay) scrollbar is shown, the scrollbar
// occupies the textarea's rightmost pixels and the absolutely-positioned // occupies the textarea's rightmost pixels and the absolutely-positioned
// corner clear (x) button would overlap it. We expose this width as a CSS // corner buttons (clear x / active-filters filter) would overlap it. We
// var so the button can shift left of the scrollbar; it is 0 when there is // expose this width as a CSS var so those buttons can shift left of the
// no scrollbar (content fits, or platform overlay scrollbars that float // scrollbar; it is 0 when there is no scrollbar (content fits, or platform
// over the content). // overlay scrollbars that float over the content).
const vScrollbarWidth = ref(0) const vScrollbarWidth = ref(0)
let scrollbarResizeObserver: ResizeObserver | null = null let scrollbarResizeObserver: ResizeObserver | null = null
@@ -111,6 +125,48 @@ const hasText = ref(false)
// Show clear button when there is text // Show clear button when there is text
const showClearButton = computed(() => hasText.value) const showClearButton = computed(() => hasText.value)
// Active-filters search indicator (loras nodes only). Mirrors the
// loramanager.lora_active_filters_autocomplete setting so users can
// discover and toggle the /activefilters mode without opening the
// dropdown or the settings dialog.
const isLorasMode = (props.modelType ?? 'loras') === 'loras'
const activeFiltersEnabled = ref(false)
const refreshActiveFiltersState = () => {
if (isLorasMode) {
activeFiltersEnabled.value = getLoraActiveFiltersAutocompletePreference()
}
}
const onSettingToggled = (event: Event) => {
const detail = (event as CustomEvent<{ settingId?: string; value?: unknown }>).detail
if (detail?.settingId === LORA_ACTIVE_FILTERS_AUTOCOMPLETE_SETTING_ID) {
activeFiltersEnabled.value = detail.value === true
}
}
const activeFiltersToggleTitle = computed(() =>
activeFiltersEnabled.value
? 'Active Filters Search is ON: suggestions respect the LoRA Manager page filters. Click to disable, or type /noactivefilters.'
: 'Active Filters Search is OFF: suggestions search the full library. Click to enable, or type /activefilters.'
)
const toggleActiveFiltersSearch = async () => {
const newValue = !activeFiltersEnabled.value
try {
const success = await setLoraManagerSettingValue(
LORA_ACTIVE_FILTERS_AUTOCOMPLETE_SETTING_ID,
newValue
)
if (!success) {
throw new Error('settings API unavailable')
}
activeFiltersEnabled.value = newValue
} catch (error) {
console.error('[Lora Manager] Failed to toggle active filters search:', error)
}
}
// Initialize autocomplete with direct ref access // Initialize autocomplete with direct ref access
useAutocomplete( useAutocomplete(
textareaRef, textareaRef,
@@ -200,9 +256,6 @@ 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 = () => { const onExternalValueChange = () => {
// The DOM value was set synchronously by the caller; the new content may
// have toggled the vertical scrollbar on/off.
updateVScrollbarWidth()
updateHasTextState() updateHasTextState()
} }
@@ -212,9 +265,6 @@ const setupWidgetOnSetValue = () => {
props.widget.onSetValue = (value: string) => { props.widget.onSetValue = (value: string) => {
// The DOM value is already set by setValue, just update our state // The DOM value is already set by setValue, just update our state
hasText.value = value.length > 0 hasText.value = value.length > 0
// Programmatic sets can toggle the scrollbar; re-measure so the
// corner clear button stays clear of it.
updateVScrollbarWidth()
} }
} }
} }
@@ -305,6 +355,11 @@ onMounted(() => {
// Setup widget.onSetValue callback // Setup widget.onSetValue callback
setupWidgetOnSetValue() setupWidgetOnSetValue()
// Active-filters indicator: read initial state and stay in sync with
// slash-command / context-menu toggles dispatched via settings.js
refreshActiveFiltersState()
window.addEventListener(SETTING_TOGGLED_EVENT_NAME, onSettingToggled)
// Keep the corner buttons clear of the textarea's vertical scrollbar. // Keep the corner buttons clear of the textarea's vertical scrollbar.
updateVScrollbarWidth() updateVScrollbarWidth()
observeScrollbarWidth() observeScrollbarWidth()
@@ -336,6 +391,7 @@ onUnmounted(() => {
// Remove event listener // Remove event listener
document.removeEventListener('lora-manager:vue-mode-change', onModeChange) document.removeEventListener('lora-manager:vue-mode-change', onModeChange)
window.removeEventListener(SETTING_TOGGLED_EVENT_NAME, onSettingToggled)
}) })
</script> </script>
@@ -428,6 +484,58 @@ onUnmounted(() => {
height: 12px; height: 12px;
} }
/* Active-filters search indicator (loras nodes only) */
.active-filters-toggle {
position: absolute;
top: 3px;
right: calc(3px + var(--lm-vscrollbar-width, 0px));
width: 16px;
height: 16px;
padding: 2px;
margin: 0;
border: none;
border-radius: 4px;
background: rgba(128, 128, 128, 0.25);
color: rgba(255, 255, 255, 0.5);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.7;
transition: opacity 0.2s ease, background-color 0.2s ease, color 0.2s ease;
z-index: 10;
}
.active-filters-toggle:hover {
opacity: 1;
background: rgba(128, 128, 128, 0.45);
color: rgba(255, 255, 255, 0.85);
}
.active-filters-toggle.is-active {
background: rgba(59, 130, 246, 0.35);
color: #7db8ff;
opacity: 1;
}
.active-filters-toggle svg {
width: 11px;
height: 11px;
}
/* Vue DOM mode adjustments for the indicator */
.text-input.vue-dom-mode ~ .active-filters-toggle {
top: 8px;
right: calc(8px + var(--lm-vscrollbar-width, 0px));
width: 20px;
height: 20px;
}
.text-input.vue-dom-mode ~ .active-filters-toggle svg {
width: 13px;
height: 13px;
}
/* Vue DOM mode adjustments for clear button */ /* Vue DOM mode adjustments for clear button */
.text-input.vue-dom-mode ~ .clear-button { .text-input.vue-dom-mode ~ .clear-button {
right: calc(8px + var(--lm-vscrollbar-width, 0px)); right: calc(8px + var(--lm-vscrollbar-width, 0px));
@@ -10,13 +10,13 @@
import { nextTick } from 'vue' import { nextTick } from 'vue'
import { shallowMount } from '@vue/test-utils' import { shallowMount } from '@vue/test-utils'
import { describe, expect, it, vi, afterEach } from 'vitest' import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'
import AutocompleteTextWidget from '@/components/AutocompleteTextWidget.vue' import AutocompleteTextWidget from '@/components/AutocompleteTextWidget.vue'
function createMockWidget() { function createMockWidget() {
return { return {
callback: vi.fn(), callback: vi.fn(),
onSetValue: undefined as ((v: string) => void) | undefined, onSetValue: undefined,
inputEl: undefined, inputEl: undefined,
metadataWidget: undefined, metadataWidget: undefined,
name: 'text', name: 'text',
@@ -135,15 +135,121 @@ describe('AutocompleteTextWidget clear button', () => {
}) })
}) })
/**
* Tests for the active-filters search indicator (loras mode only).
*
* The small filter chip in the textarea corner mirrors the
* loramanager.lora_active_filters_autocomplete setting: it reflects the
* current state, can toggle it, and stays in sync with slash-command /
* context-menu toggles via the lora-manager:setting-toggled window event.
*/
const settingsMocks = vi.hoisted(() => ({
getPreference: vi.fn(),
setValue: vi.fn(),
}))
vi.mock('../../../web/comfyui/settings.js', () => ({
LORA_ACTIVE_FILTERS_AUTOCOMPLETE_SETTING_ID:
'loramanager.lora_active_filters_autocomplete',
SETTING_TOGGLED_EVENT_NAME: 'lora-manager:setting-toggled',
getLoraActiveFiltersAutocompletePreference: settingsMocks.getPreference,
setLoraManagerSettingValue: settingsMocks.setValue,
}))
const getActiveFiltersPreferenceMock = settingsMocks.getPreference
const setSettingValueMock = settingsMocks.setValue
function mountLorasWidget() {
const widget = createMockWidget()
const node = { id: 1 }
const wrapper = shallowMount(AutocompleteTextWidget, {
props: { widget, node, modelType: 'loras' },
attachTo: document.body,
})
return { wrapper, widget }
}
describe('AutocompleteTextWidget active-filters indicator', () => {
beforeEach(() => {
getActiveFiltersPreferenceMock.mockReset()
getActiveFiltersPreferenceMock.mockReturnValue(false)
setSettingValueMock.mockReset()
setSettingValueMock.mockResolvedValue(true)
})
it('renders only in loras mode', () => {
const loras = mountLorasWidget()
expect(loras.wrapper.find('.active-filters-toggle').exists()).toBe(true)
const widget = createMockWidget()
const prompt = shallowMount(AutocompleteTextWidget, {
props: { widget, node: { id: 2 }, modelType: 'prompt' },
attachTo: document.body,
})
expect(prompt.find('.active-filters-toggle').exists()).toBe(false)
})
it('reflects the current setting state', async () => {
const { wrapper } = mountLorasWidget()
await nextTick()
expect(wrapper.find('.active-filters-toggle').classes()).not.toContain('is-active')
getActiveFiltersPreferenceMock.mockReturnValue(true)
const wrapper2 = mountLorasWidget().wrapper
await nextTick()
expect(wrapper2.find('.active-filters-toggle').classes()).toContain('is-active')
})
it('toggles the setting when clicked', async () => {
const { wrapper } = mountLorasWidget()
await nextTick()
await wrapper.find('.active-filters-toggle').trigger('click')
expect(setSettingValueMock).toHaveBeenCalledWith(
'loramanager.lora_active_filters_autocomplete',
true
)
await nextTick()
expect(wrapper.find('.active-filters-toggle').classes()).toContain('is-active')
})
it('stays in sync with setting-toggled window events', async () => {
const { wrapper } = mountLorasWidget()
await nextTick()
expect(wrapper.find('.active-filters-toggle').classes()).not.toContain('is-active')
window.dispatchEvent(
new CustomEvent('lora-manager:setting-toggled', {
detail: {
settingId: 'loramanager.lora_active_filters_autocomplete',
value: true,
},
})
)
await nextTick()
expect(wrapper.find('.active-filters-toggle').classes()).toContain('is-active')
window.dispatchEvent(
new CustomEvent('lora-manager:setting-toggled', {
detail: { settingId: 'loramanager.some_other_setting', value: true },
})
)
await nextTick()
// Unrelated settings must not flip the indicator
expect(wrapper.find('.active-filters-toggle').classes()).toContain('is-active')
})
})
/** /**
* Tests for the vertical-scrollbar inset. * Tests for the vertical-scrollbar inset.
* *
* When the textarea content overflows and a classic (non-overlay) scrollbar * When the textarea content overflows and a classic (non-overlay) scrollbar
* is shown, the absolutely-positioned corner clear (x) button would sit on * is shown, the absolutely-positioned corner buttons (clear x, active-filters
* top of the scrollbar. The component measures the scrollbar gutter and * filter chip) would sit on top of the scrollbar. The component measures the
* exposes it as the --lm-vscrollbar-width CSS var on .input-wrapper so the * scrollbar gutter and exposes it as the --lm-vscrollbar-width CSS var on
* button shifts left of the scrollbar. jsdom does no layout, so overflow is * .input-wrapper so the buttons shift left of the scrollbar. jsdom does no
* simulated by overriding the scroll/dimension props. * layout, so overflow is simulated by overriding the scroll/dimension props.
*/ */
describe('AutocompleteTextWidget vertical scrollbar inset', () => { describe('AutocompleteTextWidget vertical scrollbar inset', () => {
function overrideTextareaMetrics( function overrideTextareaMetrics(
@@ -216,66 +322,4 @@ describe('AutocompleteTextWidget vertical scrollbar inset', () => {
await nextTick() await nextTick()
expect(wrapperEl.style.getPropertyValue('--lm-vscrollbar-width')).toBe('0px') expect(wrapperEl.style.getPropertyValue('--lm-vscrollbar-width')).toBe('0px')
}) })
it('re-measures the inset when the value is set programmatically via onSetValue', async () => {
const { wrapper, widget } = mountWidget()
const textarea = wrapper.find('textarea').element as HTMLTextAreaElement
// Start with no overflow
overrideTextareaMetrics(textarea, {
scrollHeight: 100,
clientHeight: 100,
offsetWidth: 320,
clientWidth: 320,
})
await nextTick()
// Simulate an external setValue (e.g. "send lora to workflow"): the DOM
// value is set by the caller and widget.onSetValue fires without an
// input event. Content now overflows → inset must be re-measured.
overrideTextareaMetrics(textarea, {
scrollHeight: 200,
clientHeight: 100,
offsetWidth: 320,
clientWidth: 305, // 15px scrollbar gutter
})
if (!widget.onSetValue) throw new Error('onSetValue not installed by component')
widget.onSetValue('long content')
await nextTick()
const wrapperEl = wrapper.find('.input-wrapper').element as HTMLElement
expect(wrapperEl.style.getPropertyValue('--lm-vscrollbar-width')).toBe('15px')
})
it('re-measures the inset on external value-change events', async () => {
const { wrapper } = mountWidget()
const textarea = wrapper.find('textarea').element as HTMLTextAreaElement
// Start with no overflow
overrideTextareaMetrics(textarea, {
scrollHeight: 100,
clientHeight: 100,
offsetWidth: 320,
clientWidth: 320,
})
await nextTick()
// The lora-manager:autocomplete-value-changed event fires when the
// widget value is set externally; content now overflows → re-measure.
overrideTextareaMetrics(textarea, {
scrollHeight: 200,
clientHeight: 100,
offsetWidth: 320,
clientWidth: 305, // 15px scrollbar gutter
})
textarea.dispatchEvent(
new CustomEvent('lora-manager:autocomplete-value-changed', {
detail: { value: 'long content' },
})
)
await nextTick()
const wrapperEl = wrapper.find('.input-wrapper').element as HTMLElement
expect(wrapperEl.style.getPropertyValue('--lm-vscrollbar-width')).toBe('15px')
})
}) })
+9 -78
View File
@@ -1,86 +1,12 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js' import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
import { dirname, resolve } from 'path' import { resolve } from 'path'
// Specifiers that must stay external. The bundle is emitted to
// web/comfyui/vue-widgets/, and ComfyUI serves that directory's parent
// (web/comfyui) at /extensions/ComfyUI-Lora-Manager/, so one "../" from the
// bundle reaches web/comfyui modules and three "../../.." reach ComfyUI's
// own runtime scripts at runtime.
//
// scripts/app.js and scripts/api.js are intentionally NOT listed here: they
// are externalized by the keep-runtime-modules-external plugin below, which
// also rewrites the shallower "../../scripts/*" specifiers used by modules
// inlined from web/comfyui/ so every binding dedupes into a single import.
const EXTERNAL_SPECIFIERS = [
'../loras_widget.js',
'../autocomplete.js',
'../preview_tooltip.js'
]
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
vue(), vue(),
cssInjectedByJsPlugin(), // Inject CSS into JS for ComfyUI compatibility cssInjectedByJsPlugin() // Inject CSS into JS for ComfyUI compatibility
// Keep shared runtime modules external instead of inlining them into
// the bundle. This guards against the inlined-shim bug class (the
// removed active-filters chip ended up writing settings to a dead
// in-memory store this way):
//
// 1. Modules under web/comfyui/ import the repo-root scripts/app.js
// TEST SHIM as "../../scripts/app.js" — a depth that resolves to
// the shim on the build filesystem but 404s at the bundle's
// runtime location. Rewrite to the canonical bundle-depth
// specifier so every app/api binding in the bundle is the REAL
// ComfyUI module.
// 2. web/comfyui/settings.js must never be duplicated into the
// bundle: it registers settings via a module-level side effect
// and owns module state. Externalize it to "../settings.js" so
// the bundle binds to the SAME vanilla module instance that the
// ComfyUI extension loader already loaded.
{
name: 'lora-manager:keep-runtime-modules-external',
enforce: 'pre',
resolveId(source, importer) {
const scriptsMatch = source.match(/^(\.\.\/)+scripts\/(app|api)\.js$/)
if (scriptsMatch) {
return { id: `../../../scripts/${scriptsMatch[2]}.js`, external: true }
}
if (
importer &&
/[\\/]web[\\/]comfyui[\\/]settings\.js$/.test(
resolve(dirname(importer), source)
)
) {
return { id: '../settings.js', external: true }
}
return null
},
},
// Warning twin of the rewrite above: importing web/comfyui/* from
// widget source inlines that module into the bundle, duplicating any
// module-level side effects/state it owns. The settings.js and
// scripts/app|api.js imports are made safe by the plugin above, but
// review any further such import deliberately.
{
name: 'lora-manager:warn-web-comfyui-imports',
enforce: 'pre',
resolveId(source, importer) {
if (
importer &&
/[\\/]vue-widgets[\\/]src[\\/]/.test(importer) &&
/[\\/]web[\\/]comfyui[\\/]/.test(source)
) {
this.warn(
`[vue-widgets] Inlining web/comfyui module "${source}" into the bundle. ` +
'settings.js and scripts/app|api.js imports are externalized by this config, ' +
'but the inlined copy still duplicates module-level side effects — verify that is intended.'
)
}
return null
},
},
], ],
resolve: { resolve: {
alias: { alias: {
@@ -94,7 +20,13 @@ export default defineConfig({
fileName: 'lora-manager-widgets' fileName: 'lora-manager-widgets'
}, },
rollupOptions: { rollupOptions: {
external: EXTERNAL_SPECIFIERS, external: [
'../../../scripts/app.js',
'../../../scripts/api.js',
'../loras_widget.js',
'../autocomplete.js',
'../preview_tooltip.js'
],
output: { output: {
dir: '../web/comfyui/vue-widgets', dir: '../web/comfyui/vue-widgets',
entryFileNames: 'lora-manager-widgets.js', entryFileNames: 'lora-manager-widgets.js',
@@ -109,4 +41,3 @@ export default defineConfig({
'process.env.NODE_ENV': JSON.stringify('production') 'process.env.NODE_ENV': JSON.stringify('production')
} }
}) })
+49 -3
View File
@@ -14,9 +14,11 @@ import {
getAutocompleteAppendCommaPreference, getAutocompleteAppendCommaPreference,
getAutocompleteAutoFormatPreference, getAutocompleteAutoFormatPreference,
getAutocompleteAcceptKeyPreference, getAutocompleteAcceptKeyPreference,
LORA_ACTIVE_FILTERS_AUTOCOMPLETE_SETTING_ID,
getLoraActiveFiltersAutocompletePreference, getLoraActiveFiltersAutocompletePreference,
getPromptTagAutocompletePreference, getPromptTagAutocompletePreference,
getTagSpaceReplacementPreference, getTagSpaceReplacementPreference,
SETTING_TOGGLED_EVENT_NAME,
setLoraManagerSettingValue, setLoraManagerSettingValue,
} from "./settings.js"; } from "./settings.js";
import { showToast } from "./utils.js"; import { showToast } from "./utils.js";
@@ -607,6 +609,7 @@ class AutoComplete {
this.onBlur = null; this.onBlur = null;
this.onDocumentClick = null; this.onDocumentClick = null;
this.onScroll = null; this.onScroll = null;
this.onSettingToggled = null;
this.init(); this.init();
} }
@@ -774,6 +777,22 @@ class AutoComplete {
}; };
document.addEventListener('click', this.onDocumentClick); document.addEventListener('click', this.onDocumentClick);
// React to setting changes that happen underneath an open dropdown
// (e.g. toggling the active-filters search from the node's filter
// chip can keep the dropdown open in ComfyUI). Refresh the state
// hints so they never show a message for the previous state.
if (this.onSettingToggled) {
window.removeEventListener(SETTING_TOGGLED_EVENT_NAME, this.onSettingToggled);
}
this.onSettingToggled = (e) => {
const detail = e && e.detail;
if (!detail || detail.settingId !== LORA_ACTIVE_FILTERS_AUTOCOMPLETE_SETTING_ID) {
return;
}
this._refreshStateHints();
};
window.addEventListener(SETTING_TOGGLED_EVENT_NAME, this.onSettingToggled);
// Mark this element as having autocomplete events bound // Mark this element as having autocomplete events bound
this.inputElement._autocompleteEventsBound = true; this.inputElement._autocompleteEventsBound = true;
@@ -1811,15 +1830,37 @@ class AutoComplete {
} }
} }
/**
* Refresh state-dependent dropdown decorations (first-run Tip and the
* slash-command-list footer) from the live setting. Called when the
* active-filters toggle changes underneath an open dropdown, so the
* dropdown never keeps showing a message for the previous state.
*/
_refreshStateHints() {
if (!this.isVisible || this.modelType !== 'loras') {
return;
}
if (this.showingCommands) {
// Command list: footer text carries the ON/OFF state.
this._renderCommandListFooter();
} else {
// Plain suggestions: the first-run Tip advertises /activefilters
// only while the feature is disabled.
this._maybeShowFirstRunHint();
}
}
/** /**
* Show a one-time, dismissible hint inside the dropdown surfacing the * Show a one-time, dismissible hint inside the dropdown surfacing the
* toggle commands: prompt nodes advertise /noautocomplete, loras nodes * toggle commands: prompt nodes advertise /noautocomplete, loras nodes
* advertise /activefilters. Dismissal is persisted in localStorage. * advertise /activefilters. Dismissal is persisted in localStorage.
*/ */
_maybeShowFirstRunHint() { _maybeShowFirstRunHint() {
if (this.firstRunHint) { // Dropdown decorations can outlive a state change (the dropdown may
return; // stay open when the setting is toggled from the node, e.g. clicking
} // the active-filters filter chip). Always recompute from the live
// state so a stale Tip is never kept for the previous state.
this._removeFirstRunHint();
let hintText = null; let hintText = null;
let storageKey = null; let storageKey = null;
@@ -3178,6 +3219,11 @@ class AutoComplete {
this.onDocumentClick = null; this.onDocumentClick = null;
} }
if (this.onSettingToggled) {
window.removeEventListener(SETTING_TOGGLED_EVENT_NAME, this.onSettingToggled);
this.onSettingToggled = null;
}
if (this.onScroll && this.scrollContainer) { if (this.onScroll && this.scrollContainer) {
this.scrollContainer.removeEventListener('scroll', this.onScroll); this.scrollContainer.removeEventListener('scroll', this.onScroll);
this.onScroll = null; this.onScroll = null;
+21 -1
View File
@@ -175,23 +175,42 @@ const getPromptTagAutocompletePreference = (() => {
/** /**
* Persist a LoRA Manager setting through ComfyUI's setting API. * Persist a LoRA Manager setting through ComfyUI's setting API.
* Returns true when the setting was written successfully. * Returns true when the setting was written successfully.
*
* Every successful write broadcasts a "lora-manager:setting-toggled" window
* event (see SETTING_TOGGLED_EVENT_NAME) so widgets mirroring the setting
* (e.g. the active-filters indicator in the autocomplete text widget) stay in
* sync with slash-command / context-menu toggles.
*/ */
const SETTING_TOGGLED_EVENT_NAME = "lora-manager:setting-toggled";
const setLoraManagerSettingValue = async (settingId, value) => { const setLoraManagerSettingValue = async (settingId, value) => {
const settingManager = app?.extensionManager?.setting; const settingManager = app?.extensionManager?.setting;
if (settingManager && typeof settingManager.set === "function") { if (settingManager && typeof settingManager.set === "function") {
await settingManager.set(settingId, value); await settingManager.set(settingId, value);
_notifySettingToggled(settingId, value);
return true; return true;
} }
const setting = app?.ui?.settings?.settingsById?.[settingId]; const setting = app?.ui?.settings?.settingsById?.[settingId];
if (setting) { if (setting) {
app.ui.settings.setSettingValue(settingId, value); app.ui.settings.setSettingValue(settingId, value);
_notifySettingToggled(settingId, value);
return true; return true;
} }
return false; return false;
}; };
const _notifySettingToggled = (settingId, value) => {
try {
window.dispatchEvent(new CustomEvent(SETTING_TOGGLED_EVENT_NAME, {
detail: { settingId, value },
}));
} catch (error) {
// Best-effort notification; ignore non-browser environments
}
};
const getAutocompleteAppendCommaPreference = (() => { const getAutocompleteAppendCommaPreference = (() => {
let settingsUnavailableLogged = false; let settingsUnavailableLogged = false;
@@ -450,7 +469,7 @@ app.registerExtension({
name: "Search LoRA autocomplete within active filters", name: "Search LoRA autocomplete within active filters",
type: "boolean", type: "boolean",
defaultValue: LORA_ACTIVE_FILTERS_AUTOCOMPLETE_DEFAULT, defaultValue: LORA_ACTIVE_FILTERS_AUTOCOMPLETE_DEFAULT,
tooltip: "When enabled, LoRA autocomplete suggestions respect the active filters (folder/base model/tags) set in the LoRA Manager page. You can also toggle it by typing /activefilters or /noactivefilters in the LoRA field, or from the node's right-click menu.", tooltip: "When enabled, LoRA autocomplete suggestions respect the active filters (folder/base model/tags) set in the LoRA Manager page. Commands /activefilters and /noactivefilters toggle this mode.",
category: ["LoRA Manager", "Autocomplete", "LoRA Active Filters"], category: ["LoRA Manager", "Autocomplete", "LoRA Active Filters"],
}, },
{ {
@@ -598,6 +617,7 @@ app.registerExtension({
export { export {
PROMPT_TAG_AUTOCOMPLETE_SETTING_ID, PROMPT_TAG_AUTOCOMPLETE_SETTING_ID,
LORA_ACTIVE_FILTERS_AUTOCOMPLETE_SETTING_ID, LORA_ACTIVE_FILTERS_AUTOCOMPLETE_SETTING_ID,
SETTING_TOGGLED_EVENT_NAME,
getWheelSensitivity, getWheelSensitivity,
getAutoPathCorrectionPreference, getAutoPathCorrectionPreference,
getAutocompleteAppendCommaPreference, getAutocompleteAppendCommaPreference,
+234 -71
View File
@@ -2118,14 +2118,14 @@ to { transform: rotate(360deg);
padding: 20px 0; padding: 20px 0;
} }
.autocomplete-text-widget[data-v-793d67d2] { .autocomplete-text-widget[data-v-22743258] {
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-793d67d2] { .input-wrapper[data-v-22743258] {
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-793d67d2] { .text-input[data-v-22743258] {
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-793d67d2] { .text-input.vue-dom-mode[data-v-22743258] {
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-793d67d2]:focus { .text-input[data-v-22743258]:focus {
outline: none; outline: none;
} }
/* Clear button styles */ /* Clear button styles */
.clear-button[data-v-793d67d2] { .clear-button[data-v-22743258] {
position: absolute; position: absolute;
right: calc(6px + var(--lm-vscrollbar-width, 0px)); right: calc(6px + var(--lm-vscrollbar-width, 0px));
bottom: 6px; /* Changed from top to bottom */ bottom: 6px; /* Changed from top to bottom */
@@ -2189,31 +2189,79 @@ 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-793d67d2] { .input-wrapper:hover .clear-button[data-v-22743258] {
opacity: 0.7; opacity: 0.7;
pointer-events: auto; pointer-events: auto;
} }
.clear-button[data-v-793d67d2]:hover { .clear-button[data-v-22743258]:hover {
opacity: 1; opacity: 1;
background: rgba(255, 100, 100, 0.8); background: rgba(255, 100, 100, 0.8);
} }
.clear-button svg[data-v-793d67d2] { .clear-button svg[data-v-22743258] {
width: 12px; width: 12px;
height: 12px; height: 12px;
} }
/* Active-filters search indicator (loras nodes only) */
.active-filters-toggle[data-v-22743258] {
position: absolute;
top: 3px;
right: calc(3px + var(--lm-vscrollbar-width, 0px));
width: 16px;
height: 16px;
padding: 2px;
margin: 0;
border: none;
border-radius: 4px;
background: rgba(128, 128, 128, 0.25);
color: rgba(255, 255, 255, 0.5);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.7;
transition: opacity 0.2s ease, background-color 0.2s ease, color 0.2s ease;
z-index: 10;
}
.active-filters-toggle[data-v-22743258]:hover {
opacity: 1;
background: rgba(128, 128, 128, 0.45);
color: rgba(255, 255, 255, 0.85);
}
.active-filters-toggle.is-active[data-v-22743258] {
background: rgba(59, 130, 246, 0.35);
color: #7db8ff;
opacity: 1;
}
.active-filters-toggle svg[data-v-22743258] {
width: 11px;
height: 11px;
}
/* Vue DOM mode adjustments for the indicator */
.text-input.vue-dom-mode ~ .active-filters-toggle[data-v-22743258] {
top: 8px;
right: calc(8px + var(--lm-vscrollbar-width, 0px));
width: 20px;
height: 20px;
}
.text-input.vue-dom-mode ~ .active-filters-toggle svg[data-v-22743258] {
width: 13px;
height: 13px;
}
/* Vue DOM mode adjustments for clear button */ /* Vue DOM mode adjustments for clear button */
.text-input.vue-dom-mode ~ .clear-button[data-v-793d67d2] { .text-input.vue-dom-mode ~ .clear-button[data-v-22743258] {
right: calc(8px + var(--lm-vscrollbar-width, 0px)); right: calc(8px + var(--lm-vscrollbar-width, 0px));
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-793d67d2]:hover { .text-input.vue-dom-mode ~ .clear-button[data-v-22743258]:hover {
background: oklch(62% 0.18 25); background: oklch(62% 0.18 25);
} }
.text-input.vue-dom-mode ~ .clear-button svg[data-v-793d67d2] { .text-input.vue-dom-mode ~ .clear-button svg[data-v-22743258] {
width: 14px; width: 14px;
height: 14px; height: 14px;
} }
@@ -2481,9 +2529,8 @@ to { transform: rotate(360deg);
} }
})(); })();
var _a; var _a;
import { app } from "../../../scripts/app.js"; import { app as app$1 } from "../../../scripts/app.js";
import { api } from "../../../scripts/api.js"; import { api as api$1 } from "../../../scripts/api.js";
import "../settings.js";
/** /**
* @vue/shared v3.5.26 * @vue/shared v3.5.26
* (c) 2018-present Yuxi (Evan) You and Vue contributors * (c) 2018-present Yuxi (Evan) You and Vue contributors
@@ -11053,7 +11100,7 @@ const _sfc_main$o = /* @__PURE__ */ defineComponent({
const EditButton = /* @__PURE__ */ _export_sfc(_sfc_main$o, [["__scopeId", "data-v-8da8aa4b"]]); const EditButton = /* @__PURE__ */ _export_sfc(_sfc_main$o, [["__scopeId", "data-v-8da8aa4b"]]);
const _hoisted_1$k = { class: "section" }; const _hoisted_1$k = { class: "section" };
const _hoisted_2$j = { class: "section__header" }; const _hoisted_2$j = { class: "section__header" };
const _hoisted_3$g = { class: "section__content" }; const _hoisted_3$h = { class: "section__content" };
const _hoisted_4$f = { const _hoisted_4$f = {
key: 0, key: 0,
class: "section__placeholder" class: "section__placeholder"
@@ -11083,7 +11130,7 @@ const _sfc_main$n = /* @__PURE__ */ defineComponent({
onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("edit")) onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("edit"))
}) })
]), ]),
createBaseVNode("div", _hoisted_3$g, [ createBaseVNode("div", _hoisted_3$h, [
__props.selected.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_4$f, " All models ")) : (openBlock(), createElementBlock("div", _hoisted_5$d, [ __props.selected.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_4$f, " All models ")) : (openBlock(), createElementBlock("div", _hoisted_5$d, [
(openBlock(true), createElementBlock(Fragment, null, renderList(__props.selected, (name) => { (openBlock(true), createElementBlock(Fragment, null, renderList(__props.selected, (name) => {
return openBlock(), createBlock(FilterChip, { return openBlock(), createBlock(FilterChip, {
@@ -11102,7 +11149,7 @@ const _sfc_main$n = /* @__PURE__ */ defineComponent({
const BaseModelSection = /* @__PURE__ */ _export_sfc(_sfc_main$n, [["__scopeId", "data-v-12f059e2"]]); const BaseModelSection = /* @__PURE__ */ _export_sfc(_sfc_main$n, [["__scopeId", "data-v-12f059e2"]]);
const _hoisted_1$j = { class: "section" }; const _hoisted_1$j = { class: "section" };
const _hoisted_2$i = { class: "section__columns" }; const _hoisted_2$i = { class: "section__columns" };
const _hoisted_3$f = { class: "section__column" }; const _hoisted_3$g = { class: "section__column" };
const _hoisted_4$e = { class: "section__column-header" }; const _hoisted_4$e = { class: "section__column-header" };
const _hoisted_5$c = { class: "section__column-content" }; const _hoisted_5$c = { class: "section__column-content" };
const _hoisted_6$c = { const _hoisted_6$c = {
@@ -11138,7 +11185,7 @@ const _sfc_main$m = /* @__PURE__ */ defineComponent({
createBaseVNode("span", { class: "section__title" }, "TAGS") createBaseVNode("span", { class: "section__title" }, "TAGS")
], -1)), ], -1)),
createBaseVNode("div", _hoisted_2$i, [ createBaseVNode("div", _hoisted_2$i, [
createBaseVNode("div", _hoisted_3$f, [ createBaseVNode("div", _hoisted_3$g, [
createBaseVNode("div", _hoisted_4$e, [ createBaseVNode("div", _hoisted_4$e, [
_cache[2] || (_cache[2] = createBaseVNode("span", { class: "section__column-title section__column-title--include" }, "INCLUDE", -1)), _cache[2] || (_cache[2] = createBaseVNode("span", { class: "section__column-title section__column-title--include" }, "INCLUDE", -1)),
createVNode(EditButton, { createVNode(EditButton, {
@@ -11184,7 +11231,7 @@ const _sfc_main$m = /* @__PURE__ */ defineComponent({
const TagsSection = /* @__PURE__ */ _export_sfc(_sfc_main$m, [["__scopeId", "data-v-b869b780"]]); const TagsSection = /* @__PURE__ */ _export_sfc(_sfc_main$m, [["__scopeId", "data-v-b869b780"]]);
const _hoisted_1$i = { class: "section" }; const _hoisted_1$i = { class: "section" };
const _hoisted_2$h = { class: "section__columns" }; const _hoisted_2$h = { class: "section__columns" };
const _hoisted_3$e = { class: "section__column" }; const _hoisted_3$f = { class: "section__column" };
const _hoisted_4$d = { class: "section__column-header" }; const _hoisted_4$d = { class: "section__column-header" };
const _hoisted_5$b = { class: "section__content" }; const _hoisted_5$b = { class: "section__content" };
const _hoisted_6$b = { const _hoisted_6$b = {
@@ -11232,7 +11279,7 @@ const _sfc_main$l = /* @__PURE__ */ defineComponent({
createBaseVNode("span", { class: "section__title" }, "FOLDERS") createBaseVNode("span", { class: "section__title" }, "FOLDERS")
], -1)), ], -1)),
createBaseVNode("div", _hoisted_2$h, [ createBaseVNode("div", _hoisted_2$h, [
createBaseVNode("div", _hoisted_3$e, [ createBaseVNode("div", _hoisted_3$f, [
createBaseVNode("div", _hoisted_4$d, [ createBaseVNode("div", _hoisted_4$d, [
_cache[3] || (_cache[3] = createBaseVNode("span", { class: "section__column-title section__column-title--include" }, "INCLUDE", -1)), _cache[3] || (_cache[3] = createBaseVNode("span", { class: "section__column-title section__column-title--include" }, "INCLUDE", -1)),
createBaseVNode("button", { createBaseVNode("button", {
@@ -11300,7 +11347,7 @@ const _sfc_main$l = /* @__PURE__ */ defineComponent({
const FoldersSection = /* @__PURE__ */ _export_sfc(_sfc_main$l, [["__scopeId", "data-v-af9caf84"]]); const FoldersSection = /* @__PURE__ */ _export_sfc(_sfc_main$l, [["__scopeId", "data-v-af9caf84"]]);
const _hoisted_1$h = { class: "section" }; const _hoisted_1$h = { class: "section" };
const _hoisted_2$g = { class: "section__header" }; const _hoisted_2$g = { class: "section__header" };
const _hoisted_3$d = { class: "section__toggle" }; const _hoisted_3$e = { class: "section__toggle" };
const _hoisted_4$c = ["checked"]; const _hoisted_4$c = ["checked"];
const _hoisted_5$a = { class: "section__columns" }; const _hoisted_5$a = { class: "section__columns" };
const _hoisted_6$a = { class: "section__column" }; const _hoisted_6$a = { class: "section__column" };
@@ -11356,7 +11403,7 @@ const _sfc_main$k = /* @__PURE__ */ defineComponent({
return openBlock(), createElementBlock("div", _hoisted_1$h, [ return openBlock(), createElementBlock("div", _hoisted_1$h, [
createBaseVNode("div", _hoisted_2$g, [ createBaseVNode("div", _hoisted_2$g, [
_cache[4] || (_cache[4] = createBaseVNode("span", { class: "section__title" }, "NAME PATTERNS", -1)), _cache[4] || (_cache[4] = createBaseVNode("span", { class: "section__title" }, "NAME PATTERNS", -1)),
createBaseVNode("label", _hoisted_3$d, [ createBaseVNode("label", _hoisted_3$e, [
createBaseVNode("input", { createBaseVNode("input", {
type: "checkbox", type: "checkbox",
checked: __props.useRegex, checked: __props.useRegex,
@@ -11440,7 +11487,7 @@ const _sfc_main$k = /* @__PURE__ */ defineComponent({
const NamePatternsSection = /* @__PURE__ */ _export_sfc(_sfc_main$k, [["__scopeId", "data-v-9995b5ed"]]); const NamePatternsSection = /* @__PURE__ */ _export_sfc(_sfc_main$k, [["__scopeId", "data-v-9995b5ed"]]);
const _hoisted_1$g = { class: "section" }; const _hoisted_1$g = { class: "section" };
const _hoisted_2$f = { class: "section__toggles" }; const _hoisted_2$f = { class: "section__toggles" };
const _hoisted_3$c = { class: "toggle-item" }; const _hoisted_3$d = { class: "toggle-item" };
const _hoisted_4$b = ["aria-checked"]; const _hoisted_4$b = ["aria-checked"];
const _hoisted_5$9 = { class: "toggle-item" }; const _hoisted_5$9 = { class: "toggle-item" };
const _hoisted_6$9 = ["aria-checked"]; const _hoisted_6$9 = ["aria-checked"];
@@ -11458,7 +11505,7 @@ const _sfc_main$j = /* @__PURE__ */ defineComponent({
createBaseVNode("span", { class: "section__title" }, "LICENSE") createBaseVNode("span", { class: "section__title" }, "LICENSE")
], -1)), ], -1)),
createBaseVNode("div", _hoisted_2$f, [ createBaseVNode("div", _hoisted_2$f, [
createBaseVNode("label", _hoisted_3$c, [ createBaseVNode("label", _hoisted_3$d, [
_cache[3] || (_cache[3] = createBaseVNode("span", { _cache[3] || (_cache[3] = createBaseVNode("span", {
class: "toggle-item__label", class: "toggle-item__label",
title: "Use the model without crediting the creator" title: "Use the model without crediting the creator"
@@ -11498,7 +11545,7 @@ const _sfc_main$j = /* @__PURE__ */ defineComponent({
const LicenseSection = /* @__PURE__ */ _export_sfc(_sfc_main$j, [["__scopeId", "data-v-07ddd3df"]]); const LicenseSection = /* @__PURE__ */ _export_sfc(_sfc_main$j, [["__scopeId", "data-v-07ddd3df"]]);
const _hoisted_1$f = { class: "preview" }; const _hoisted_1$f = { class: "preview" };
const _hoisted_2$e = { class: "preview__title" }; const _hoisted_2$e = { class: "preview__title" };
const _hoisted_3$b = ["disabled"]; const _hoisted_3$c = ["disabled"];
const _hoisted_4$a = { const _hoisted_4$a = {
key: 0, key: 0,
class: "preview__tooltip" class: "preview__tooltip"
@@ -11557,7 +11604,7 @@ const _sfc_main$i = /* @__PURE__ */ defineComponent({
d: "M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z" d: "M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z"
}) })
], -1) ], -1)
])], 10, _hoisted_3$b) ])], 10, _hoisted_3$c)
], 32), ], 32),
createVNode(Transition, { name: "tooltip" }, { createVNode(Transition, { name: "tooltip" }, {
default: withCtx(() => [ default: withCtx(() => [
@@ -11669,7 +11716,7 @@ const _sfc_main$h = /* @__PURE__ */ defineComponent({
const LoraPoolSummaryView = /* @__PURE__ */ _export_sfc(_sfc_main$h, [["__scopeId", "data-v-83235a00"]]); const LoraPoolSummaryView = /* @__PURE__ */ _export_sfc(_sfc_main$h, [["__scopeId", "data-v-83235a00"]]);
const _hoisted_1$d = { class: "lora-pool-modal__header" }; const _hoisted_1$d = { class: "lora-pool-modal__header" };
const _hoisted_2$c = { class: "lora-pool-modal__title-container" }; const _hoisted_2$c = { class: "lora-pool-modal__title-container" };
const _hoisted_3$a = { class: "lora-pool-modal__title" }; const _hoisted_3$b = { class: "lora-pool-modal__title" };
const _hoisted_4$9 = { const _hoisted_4$9 = {
key: 0, key: 0,
class: "lora-pool-modal__subtitle" class: "lora-pool-modal__subtitle"
@@ -11729,7 +11776,7 @@ const _sfc_main$g = /* @__PURE__ */ defineComponent({
}, [ }, [
createBaseVNode("div", _hoisted_1$d, [ createBaseVNode("div", _hoisted_1$d, [
createBaseVNode("div", _hoisted_2$c, [ createBaseVNode("div", _hoisted_2$c, [
createBaseVNode("h3", _hoisted_3$a, toDisplayString(__props.title), 1), createBaseVNode("h3", _hoisted_3$b, toDisplayString(__props.title), 1),
__props.subtitle ? (openBlock(), createElementBlock("p", _hoisted_4$9, toDisplayString(__props.subtitle), 1)) : createCommentVNode("", true) __props.subtitle ? (openBlock(), createElementBlock("p", _hoisted_4$9, toDisplayString(__props.subtitle), 1)) : createCommentVNode("", true)
]), ]),
createBaseVNode("button", { createBaseVNode("button", {
@@ -11757,7 +11804,7 @@ const _sfc_main$g = /* @__PURE__ */ defineComponent({
const ModalWrapper = /* @__PURE__ */ _export_sfc(_sfc_main$g, [["__scopeId", "data-v-7b4de03d"]]); const ModalWrapper = /* @__PURE__ */ _export_sfc(_sfc_main$g, [["__scopeId", "data-v-7b4de03d"]]);
const _hoisted_1$c = { class: "search-container" }; const _hoisted_1$c = { class: "search-container" };
const _hoisted_2$b = { class: "model-list" }; const _hoisted_2$b = { class: "model-list" };
const _hoisted_3$9 = ["checked", "onChange"]; const _hoisted_3$a = ["checked", "onChange"];
const _hoisted_4$8 = { class: "model-checkbox-visual" }; const _hoisted_4$8 = { class: "model-checkbox-visual" };
const _hoisted_5$6 = { const _hoisted_5$6 = {
key: 0, key: 0,
@@ -11867,7 +11914,7 @@ const _sfc_main$f = /* @__PURE__ */ defineComponent({
checked: isSelected(model.name), checked: isSelected(model.name),
onChange: ($event) => toggleModel(model.name), onChange: ($event) => toggleModel(model.name),
class: "model-checkbox" class: "model-checkbox"
}, null, 40, _hoisted_3$9), }, null, 40, _hoisted_3$a),
createBaseVNode("span", _hoisted_4$8, [ createBaseVNode("span", _hoisted_4$8, [
isSelected(model.name) ? (openBlock(), createElementBlock("svg", _hoisted_5$6, [..._cache[4] || (_cache[4] = [ isSelected(model.name) ? (openBlock(), createElementBlock("svg", _hoisted_5$6, [..._cache[4] || (_cache[4] = [
createBaseVNode("path", { d: "M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z" }, null, -1) createBaseVNode("path", { d: "M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z" }, null, -1)
@@ -11888,7 +11935,7 @@ const _sfc_main$f = /* @__PURE__ */ defineComponent({
const BaseModelModal = /* @__PURE__ */ _export_sfc(_sfc_main$f, [["__scopeId", "data-v-e02ca44a"]]); const BaseModelModal = /* @__PURE__ */ _export_sfc(_sfc_main$f, [["__scopeId", "data-v-e02ca44a"]]);
const _hoisted_1$b = { class: "search-container" }; const _hoisted_1$b = { class: "search-container" };
const _hoisted_2$a = ["onClick"]; const _hoisted_2$a = ["onClick"];
const _hoisted_3$8 = { const _hoisted_3$9 = {
key: 0, key: 0,
class: "no-results" class: "no-results"
}; };
@@ -12032,7 +12079,7 @@ const _sfc_main$e = /* @__PURE__ */ defineComponent({
onClick: ($event) => toggleTag(tag.tag) onClick: ($event) => toggleTag(tag.tag)
}, toDisplayString(tag.tag), 11, _hoisted_2$a); }, toDisplayString(tag.tag), 11, _hoisted_2$a);
}), 128)), }), 128)),
visibleTags.value.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_3$8, " No tags found ")) : createCommentVNode("", true), visibleTags.value.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_3$9, " No tags found ")) : createCommentVNode("", true),
hasMoreTags.value ? (openBlock(), createElementBlock("div", _hoisted_4$7, " Scroll to load more... ")) : createCommentVNode("", true) hasMoreTags.value ? (openBlock(), createElementBlock("div", _hoisted_4$7, " Scroll to load more... ")) : createCommentVNode("", true)
], 544) ], 544)
]), ]),
@@ -12047,7 +12094,7 @@ const _hoisted_2$9 = {
key: 1, key: 1,
class: "tree-node__toggle-spacer" class: "tree-node__toggle-spacer"
}; };
const _hoisted_3$7 = { class: "tree-node__checkbox-label" }; const _hoisted_3$8 = { class: "tree-node__checkbox-label" };
const _hoisted_4$6 = ["checked"]; const _hoisted_4$6 = ["checked"];
const _hoisted_5$5 = { const _hoisted_5$5 = {
key: 0, key: 0,
@@ -12108,7 +12155,7 @@ const _sfc_main$d = /* @__PURE__ */ defineComponent({
createBaseVNode("path", { d: "M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z" }, null, -1) createBaseVNode("path", { d: "M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z" }, null, -1)
])], 2)) ])], 2))
])) : (openBlock(), createElementBlock("span", _hoisted_2$9)), ])) : (openBlock(), createElementBlock("span", _hoisted_2$9)),
createBaseVNode("label", _hoisted_3$7, [ createBaseVNode("label", _hoisted_3$8, [
createBaseVNode("input", { createBaseVNode("input", {
type: "checkbox", type: "checkbox",
class: "tree-node__checkbox", class: "tree-node__checkbox",
@@ -12154,7 +12201,7 @@ const _sfc_main$d = /* @__PURE__ */ defineComponent({
const FolderTreeNode = /* @__PURE__ */ _export_sfc(_sfc_main$d, [["__scopeId", "data-v-90187dd4"]]); const FolderTreeNode = /* @__PURE__ */ _export_sfc(_sfc_main$d, [["__scopeId", "data-v-90187dd4"]]);
const _hoisted_1$9 = { class: "search-container" }; const _hoisted_1$9 = { class: "search-container" };
const _hoisted_2$8 = { class: "folder-tree" }; const _hoisted_2$8 = { class: "folder-tree" };
const _hoisted_3$6 = { const _hoisted_3$7 = {
key: 1, key: 1,
class: "no-results" class: "no-results"
}; };
@@ -12253,7 +12300,7 @@ const _sfc_main$c = /* @__PURE__ */ defineComponent({
onToggleExpand: toggleExpand, onToggleExpand: toggleExpand,
onToggleSelect: toggleSelect onToggleSelect: toggleSelect
}, null, 8, ["node", "selected", "expanded", "variant"]); }, null, 8, ["node", "selected", "expanded", "variant"]);
}), 128)) : (openBlock(), createElementBlock("div", _hoisted_3$6, " No folders found ")) }), 128)) : (openBlock(), createElementBlock("div", _hoisted_3$7, " No folders found "))
]) ])
]), ]),
_: 1 _: 1
@@ -12659,7 +12706,7 @@ const _sfc_main$b = /* @__PURE__ */ defineComponent({
const LoraPoolWidget = /* @__PURE__ */ _export_sfc(_sfc_main$b, [["__scopeId", "data-v-ed73eab5"]]); const LoraPoolWidget = /* @__PURE__ */ _export_sfc(_sfc_main$b, [["__scopeId", "data-v-ed73eab5"]]);
const _hoisted_1$8 = { class: "last-used-preview" }; const _hoisted_1$8 = { class: "last-used-preview" };
const _hoisted_2$7 = { class: "last-used-preview__content" }; const _hoisted_2$7 = { class: "last-used-preview__content" };
const _hoisted_3$5 = ["src", "onError"]; const _hoisted_3$6 = ["src", "onError"];
const _hoisted_4$5 = { const _hoisted_4$5 = {
key: 1, key: 1,
class: "last-used-preview__thumb last-used-preview__thumb--placeholder" class: "last-used-preview__thumb last-used-preview__thumb--placeholder"
@@ -12711,7 +12758,7 @@ const _sfc_main$a = /* @__PURE__ */ defineComponent({
src: previewUrls.value[lora.name], src: previewUrls.value[lora.name],
class: "last-used-preview__thumb", class: "last-used-preview__thumb",
onError: ($event) => onImageError(lora.name) onError: ($event) => onImageError(lora.name)
}, null, 40, _hoisted_3$5)) : (openBlock(), createElementBlock("div", _hoisted_4$5, [..._cache[0] || (_cache[0] = [ }, null, 40, _hoisted_3$6)) : (openBlock(), createElementBlock("div", _hoisted_4$5, [..._cache[0] || (_cache[0] = [
createBaseVNode("svg", { createBaseVNode("svg", {
viewBox: "0 0 16 16", viewBox: "0 0 16 16",
fill: "currentColor" fill: "currentColor"
@@ -13152,7 +13199,7 @@ const _sfc_main$8 = /* @__PURE__ */ defineComponent({
const DualRangeSlider = /* @__PURE__ */ _export_sfc(_sfc_main$8, [["__scopeId", "data-v-e0c8dc9f"]]); const DualRangeSlider = /* @__PURE__ */ _export_sfc(_sfc_main$8, [["__scopeId", "data-v-e0c8dc9f"]]);
const _hoisted_1$5 = { class: "randomizer-settings" }; const _hoisted_1$5 = { class: "randomizer-settings" };
const _hoisted_2$5 = { class: "setting-section" }; const _hoisted_2$5 = { class: "setting-section" };
const _hoisted_3$4 = { class: "count-mode-tabs" }; const _hoisted_3$5 = { class: "count-mode-tabs" };
const _hoisted_4$4 = ["checked"]; const _hoisted_4$4 = ["checked"];
const _hoisted_5$3 = ["checked"]; const _hoisted_5$3 = ["checked"];
const _hoisted_6$3 = { class: "slider-container" }; const _hoisted_6$3 = { class: "slider-container" };
@@ -13217,7 +13264,7 @@ const _sfc_main$7 = /* @__PURE__ */ defineComponent({
], -1)), ], -1)),
createBaseVNode("div", _hoisted_2$5, [ createBaseVNode("div", _hoisted_2$5, [
_cache[20] || (_cache[20] = createBaseVNode("label", { class: "setting-label" }, "LoRA Count", -1)), _cache[20] || (_cache[20] = createBaseVNode("label", { class: "setting-label" }, "LoRA Count", -1)),
createBaseVNode("div", _hoisted_3$4, [ createBaseVNode("div", _hoisted_3$5, [
createBaseVNode("label", { createBaseVNode("label", {
class: normalizeClass(["count-mode-tab", { active: __props.countMode === "fixed" }]) class: normalizeClass(["count-mode-tab", { active: __props.countMode === "fixed" }])
}, [ }, [
@@ -13848,7 +13895,7 @@ const _sfc_main$6 = /* @__PURE__ */ defineComponent({
const LoraRandomizerWidget = /* @__PURE__ */ _export_sfc(_sfc_main$6, [["__scopeId", "data-v-ca6e8cec"]]); const LoraRandomizerWidget = /* @__PURE__ */ _export_sfc(_sfc_main$6, [["__scopeId", "data-v-ca6e8cec"]]);
const _hoisted_1$4 = { class: "cycler-settings" }; const _hoisted_1$4 = { class: "cycler-settings" };
const _hoisted_2$4 = { class: "setting-section progress-section" }; const _hoisted_2$4 = { class: "setting-section progress-section" };
const _hoisted_3$3 = { class: "progress-label" }; const _hoisted_3$4 = { class: "progress-label" };
const _hoisted_4$3 = ["title"]; const _hoisted_4$3 = ["title"];
const _hoisted_5$2 = { class: "progress-counter" }; const _hoisted_5$2 = { class: "progress-counter" };
const _hoisted_6$2 = { class: "progress-index" }; const _hoisted_6$2 = { class: "progress-index" };
@@ -13973,7 +14020,7 @@ const _sfc_main$5 = /* @__PURE__ */ defineComponent({
class: normalizeClass(["progress-info", { disabled: __props.isPauseDisabled }]), class: normalizeClass(["progress-info", { disabled: __props.isPauseDisabled }]),
onClick: handleOpenSelector onClick: handleOpenSelector
}, [ }, [
createBaseVNode("span", _hoisted_3$3, toDisplayString(__props.isWorkflowExecuting ? "Using LoRA:" : "Next LoRA:"), 1), createBaseVNode("span", _hoisted_3$4, toDisplayString(__props.isWorkflowExecuting ? "Using LoRA:" : "Next LoRA:"), 1),
createBaseVNode("span", { createBaseVNode("span", {
class: normalizeClass(["progress-name clickable", { disabled: __props.isPauseDisabled, "no-lora": __props.isNoLora }]), class: normalizeClass(["progress-name clickable", { disabled: __props.isPauseDisabled, "no-lora": __props.isNoLora }]),
title: __props.currentLoraFilename title: __props.currentLoraFilename
@@ -14173,7 +14220,7 @@ const _sfc_main$5 = /* @__PURE__ */ defineComponent({
const LoraCyclerSettingsView = /* @__PURE__ */ _export_sfc(_sfc_main$5, [["__scopeId", "data-v-f0663be4"]]); const LoraCyclerSettingsView = /* @__PURE__ */ _export_sfc(_sfc_main$5, [["__scopeId", "data-v-f0663be4"]]);
const _hoisted_1$3 = { class: "search-container" }; const _hoisted_1$3 = { class: "search-container" };
const _hoisted_2$3 = { class: "lora-list" }; const _hoisted_2$3 = { class: "lora-list" };
const _hoisted_3$2 = ["onMouseenter", "onClick"]; const _hoisted_3$3 = ["onMouseenter", "onClick"];
const _hoisted_4$2 = { class: "lora-index" }; const _hoisted_4$2 = { class: "lora-index" };
const _hoisted_5$1 = ["title"]; const _hoisted_5$1 = ["title"];
const _hoisted_6$1 = { const _hoisted_6$1 = {
@@ -14354,7 +14401,7 @@ const _sfc_main$4 = /* @__PURE__ */ defineComponent({
title: item.lora.file_name title: item.lora.file_name
}, toDisplayString(item.lora.file_name), 9, _hoisted_5$1), }, toDisplayString(item.lora.file_name), 9, _hoisted_5$1),
__props.currentIndex === item.index ? (openBlock(), createElementBlock("span", _hoisted_6$1, "Current")) : createCommentVNode("", true) __props.currentIndex === item.index ? (openBlock(), createElementBlock("span", _hoisted_6$1, "Current")) : createCommentVNode("", true)
], 42, _hoisted_3$2); ], 42, _hoisted_3$3);
}), 128)), }), 128)),
filteredList.value.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_7$1, " No LoRAs found ")) : createCommentVNode("", true) filteredList.value.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_7$1, " No LoRAs found ")) : createCommentVNode("", true)
]) ])
@@ -14970,7 +15017,7 @@ const _hoisted_2$2 = {
class: "json-content", class: "json-content",
ref: "contentRef" ref: "contentRef"
}; };
const _hoisted_3$1 = ["innerHTML"]; const _hoisted_3$2 = ["innerHTML"];
const _hoisted_4$1 = { const _hoisted_4$1 = {
key: 1, key: 1,
class: "placeholder" class: "placeholder"
@@ -15065,7 +15112,7 @@ const _sfc_main$2 = /* @__PURE__ */ defineComponent({
hasMetadata.value ? (openBlock(), createElementBlock("pre", { hasMetadata.value ? (openBlock(), createElementBlock("pre", {
key: 0, key: 0,
innerHTML: highlightedJson.value innerHTML: highlightedJson.value
}, null, 8, _hoisted_3$1)) : (openBlock(), createElementBlock("div", _hoisted_4$1, "No metadata available")) }, null, 8, _hoisted_3$2)) : (openBlock(), createElementBlock("div", _hoisted_4$1, "No metadata available"))
], 512) ], 512)
]); ]);
}; };
@@ -15137,8 +15184,71 @@ function useAutocomplete(textareaRef, modelType = "loras", options = {}) {
refreshCaretHelper refreshCaretHelper
}; };
} }
const settingsStore = /* @__PURE__ */ new Map();
const app = {
extensionManager: {
setting: {
get: (id) => settingsStore.has(id) ? settingsStore.get(id) : void 0,
set: async (id, value) => {
settingsStore.set(id, value);
}
}
}
};
const LORA_ACTIVE_FILTERS_AUTOCOMPLETE_SETTING_ID = "loramanager.lora_active_filters_autocomplete";
const LORA_ACTIVE_FILTERS_AUTOCOMPLETE_DEFAULT = false;
const SETTING_TOGGLED_EVENT_NAME = "lora-manager:setting-toggled";
const setLoraManagerSettingValue = async (settingId, value) => {
var _a2, _b, _c, _d;
const settingManager = (_a2 = app == null ? void 0 : app.extensionManager) == null ? void 0 : _a2.setting;
if (settingManager && typeof settingManager.set === "function") {
await settingManager.set(settingId, value);
_notifySettingToggled(settingId, value);
return true;
}
const setting = (_d = (_c = (_b = app == null ? void 0 : app.ui) == null ? void 0 : _b.settings) == null ? void 0 : _c.settingsById) == null ? void 0 : _d[settingId];
if (setting) {
app.ui.settings.setSettingValue(settingId, value);
_notifySettingToggled(settingId, value);
return true;
}
return false;
};
const _notifySettingToggled = (settingId, value) => {
try {
window.dispatchEvent(new CustomEvent(SETTING_TOGGLED_EVENT_NAME, {
detail: { settingId, value }
}));
} catch (error) {
}
};
const getLoraActiveFiltersAutocompletePreference = /* @__PURE__ */ (() => {
let settingsUnavailableLogged = false;
return () => {
var _a2;
const settingManager = (_a2 = app == null ? void 0 : app.extensionManager) == null ? void 0 : _a2.setting;
if (!settingManager || typeof settingManager.get !== "function") {
if (!settingsUnavailableLogged) {
console.warn("LoRA Manager: settings API unavailable, using default lora active filters autocomplete setting.");
settingsUnavailableLogged = true;
}
return LORA_ACTIVE_FILTERS_AUTOCOMPLETE_DEFAULT;
}
try {
const value = settingManager.get(LORA_ACTIVE_FILTERS_AUTOCOMPLETE_SETTING_ID);
return value ?? LORA_ACTIVE_FILTERS_AUTOCOMPLETE_DEFAULT;
} catch (error) {
if (!settingsUnavailableLogged) {
console.warn("LoRA Manager: unable to read lora active filters autocomplete setting, using default.", error);
settingsUnavailableLogged = true;
}
return LORA_ACTIVE_FILTERS_AUTOCOMPLETE_DEFAULT;
}
};
})();
const _hoisted_1$1 = { class: "autocomplete-text-widget" }; const _hoisted_1$1 = { class: "autocomplete-text-widget" };
const _hoisted_2$1 = ["placeholder", "spellcheck"]; const _hoisted_2$1 = ["placeholder", "spellcheck"];
const _hoisted_3$1 = ["title"];
const _sfc_main$1 = /* @__PURE__ */ defineComponent({ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
__name: "AutocompleteTextWidget", __name: "AutocompleteTextWidget",
props: { props: {
@@ -15187,6 +15297,37 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
}; };
const hasText = ref(false); const hasText = ref(false);
const showClearButton = computed(() => hasText.value); const showClearButton = computed(() => hasText.value);
const isLorasMode = (props.modelType ?? "loras") === "loras";
const activeFiltersEnabled = ref(false);
const refreshActiveFiltersState = () => {
if (isLorasMode) {
activeFiltersEnabled.value = getLoraActiveFiltersAutocompletePreference();
}
};
const onSettingToggled = (event) => {
const detail = event.detail;
if ((detail == null ? void 0 : detail.settingId) === LORA_ACTIVE_FILTERS_AUTOCOMPLETE_SETTING_ID) {
activeFiltersEnabled.value = detail.value === true;
}
};
const activeFiltersToggleTitle = computed(
() => activeFiltersEnabled.value ? "Active Filters Search is ON: suggestions respect the LoRA Manager page filters. Click to disable, or type /noactivefilters." : "Active Filters Search is OFF: suggestions search the full library. Click to enable, or type /activefilters."
);
const toggleActiveFiltersSearch = async () => {
const newValue = !activeFiltersEnabled.value;
try {
const success = await setLoraManagerSettingValue(
LORA_ACTIVE_FILTERS_AUTOCOMPLETE_SETTING_ID,
newValue
);
if (!success) {
throw new Error("settings API unavailable");
}
activeFiltersEnabled.value = newValue;
} catch (error) {
console.error("[Lora Manager] Failed to toggle active filters search:", error);
}
};
useAutocomplete( useAutocomplete(
textareaRef, textareaRef,
props.modelType ?? "loras", props.modelType ?? "loras",
@@ -15239,14 +15380,12 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
} }
}; };
const onExternalValueChange = () => { const onExternalValueChange = () => {
updateVScrollbarWidth();
updateHasTextState(); updateHasTextState();
}; };
const setupWidgetOnSetValue = () => { const setupWidgetOnSetValue = () => {
if (props.widget) { if (props.widget) {
props.widget.onSetValue = (value) => { props.widget.onSetValue = (value) => {
hasText.value = value.length > 0; hasText.value = value.length > 0;
updateVScrollbarWidth();
}; };
} }
}; };
@@ -15296,6 +15435,8 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
props.widget.callback(textareaRef.value.value); props.widget.callback(textareaRef.value.value);
} }
setupWidgetOnSetValue(); setupWidgetOnSetValue();
refreshActiveFiltersState();
window.addEventListener(SETTING_TOGGLED_EVENT_NAME, onSettingToggled);
updateVScrollbarWidth(); updateVScrollbarWidth();
observeScrollbarWidth(); observeScrollbarWidth();
document.addEventListener("lora-manager:vue-mode-change", onModeChange); document.addEventListener("lora-manager:vue-mode-change", onModeChange);
@@ -15315,6 +15456,7 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
props.widget.onSetValue = void 0; props.widget.onSetValue = void 0;
} }
document.removeEventListener("lora-manager:vue-mode-change", onModeChange); document.removeEventListener("lora-manager:vue-mode-change", onModeChange);
window.removeEventListener(SETTING_TOGGLED_EVENT_NAME, onSettingToggled);
}); });
return (_ctx, _cache) => { return (_ctx, _cache) => {
return openBlock(), createElementBlock("div", _hoisted_1$1, [ return openBlock(), createElementBlock("div", _hoisted_1$1, [
@@ -15361,13 +15503,29 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
y2: "18" y2: "18"
}) })
], -1) ], -1)
])])) : createCommentVNode("", true) ])])) : createCommentVNode("", true),
isLorasMode ? (openBlock(), createElementBlock("button", {
key: 1,
type: "button",
class: normalizeClass(["active-filters-toggle", { "is-active": activeFiltersEnabled.value }]),
title: activeFiltersToggleTitle.value,
onClick: toggleActiveFiltersSearch
}, [..._cache[1] || (_cache[1] = [
createBaseVNode("svg", {
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2"
}, [
createBaseVNode("polygon", { points: "22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" })
], -1)
])], 10, _hoisted_3$1)) : createCommentVNode("", true)
], 4) ], 4)
]); ]);
}; };
} }
}); });
const AutocompleteTextWidget = /* @__PURE__ */ _export_sfc(_sfc_main$1, [["__scopeId", "data-v-793d67d2"]]); const AutocompleteTextWidget = /* @__PURE__ */ _export_sfc(_sfc_main$1, [["__scopeId", "data-v-22743258"]]);
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" };
@@ -15784,6 +15942,11 @@ function createModeChangeCallback(node, updateDownstreamLoaders2, nodeSpecificCa
updateDownstreamLoaders2(node); updateDownstreamLoaders2(node);
}; };
} }
const api = {
fetchApi: (...args) => fetch(...args),
addEventListener: (eventName, handler) => document.addEventListener(eventName, handler),
removeEventListener: (eventName, handler) => document.removeEventListener(eventName, handler)
};
let _loraSyntaxFormatCache = null; let _loraSyntaxFormatCache = null;
let _loraSyntaxFormatRefreshPromise = null; let _loraSyntaxFormatRefreshPromise = null;
async function _fetchLoraSyntaxFormat() { async function _fetchLoraSyntaxFormat() {
@@ -16140,8 +16303,8 @@ const AUTOCOMPLETE_TEXT_MIN_WIDTH_DEFAULT = 400;
const AUTOCOMPLETE_TEXT_MIN_HEIGHT_DEFAULT = 300; 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";
const originalGraphToPrompt = app.graphToPrompt.bind(app); const originalGraphToPrompt = app$1.graphToPrompt.bind(app$1);
app.graphToPrompt = async (...args) => { app$1.graphToPrompt = async (...args) => {
const result = await originalGraphToPrompt(...args); const result = await originalGraphToPrompt(...args);
stripAutocompleteMetadataFromPromptResult(result); stripAutocompleteMetadataFromPromptResult(result);
return result; return result;
@@ -16150,7 +16313,7 @@ function forwardMiddleMouseToCanvas(container) {
if (!container) return; if (!container) return;
container.addEventListener("pointerdown", (event) => { container.addEventListener("pointerdown", (event) => {
if (event.button === 1) { if (event.button === 1) {
const canvas = app.canvas; const canvas = app$1.canvas;
if (canvas && typeof canvas.processMouseDown === "function") { if (canvas && typeof canvas.processMouseDown === "function") {
canvas.processMouseDown(event); canvas.processMouseDown(event);
} }
@@ -16158,7 +16321,7 @@ function forwardMiddleMouseToCanvas(container) {
}); });
container.addEventListener("pointermove", (event) => { container.addEventListener("pointermove", (event) => {
if ((event.buttons & 4) === 4) { if ((event.buttons & 4) === 4) {
const canvas = app.canvas; const canvas = app$1.canvas;
if (canvas && typeof canvas.processMouseMove === "function") { if (canvas && typeof canvas.processMouseMove === "function") {
canvas.processMouseMove(event); canvas.processMouseMove(event);
} }
@@ -16166,7 +16329,7 @@ function forwardMiddleMouseToCanvas(container) {
}); });
container.addEventListener("pointerup", (event) => { container.addEventListener("pointerup", (event) => {
if (event.button === 1) { if (event.button === 1) {
const canvas = app.canvas; const canvas = app$1.canvas;
if (canvas && typeof canvas.processMouseUp === "function") { if (canvas && typeof canvas.processMouseUp === "function") {
canvas.processMouseUp(event); canvas.processMouseUp(event);
} }
@@ -16284,7 +16447,7 @@ function createLoraRandomizerWidget(node) {
const vueApp = createApp(LoraRandomizerWidget, { const vueApp = createApp(LoraRandomizerWidget, {
widget, widget,
node, node,
api api: api$1
}); });
vueApp.use(PrimeVue, { vueApp.use(PrimeVue, {
unstyled: true, unstyled: true,
@@ -16359,7 +16522,7 @@ function createLoraCyclerWidget(node) {
const vueApp = createApp(LoraCyclerWidget, { const vueApp = createApp(LoraCyclerWidget, {
widget, widget,
node, node,
api api: api$1
}); });
vueApp.use(PrimeVue, { vueApp.use(PrimeVue, {
unstyled: true, unstyled: true,
@@ -16547,13 +16710,13 @@ function applyAutocompleteTextLayoutFix(widget, _container, isVueMode) {
} }
const initVueDomModeListener = () => { const initVueDomModeListener = () => {
var _a2, _b; var _a2, _b;
if ((_b = (_a2 = app.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) {
app.ui.settings.addEventListener("Comfy.VueNodes.Enabled.change", () => { app$1.ui.settings.addEventListener("Comfy.VueNodes.Enabled.change", () => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
var _a3, _b2, _c, _d, _e2, _f; var _a3, _b2, _c, _d, _e2, _f;
const isVueDomMode = ((_c = (_b2 = (_a3 = app.ui) == null ? void 0 : _a3.settings) == null ? void 0 : _b2.getSettingValue) == null ? void 0 : _c.call(_b2, "Comfy.VueNodes.Enabled")) ?? false; const isVueDomMode = ((_c = (_b2 = (_a3 = app$1.ui) == null ? void 0 : _a3.settings) == null ? void 0 : _b2.getSettingValue) == null ? void 0 : _c.call(_b2, "Comfy.VueNodes.Enabled")) ?? false;
if ((_d = app.graph) == null ? void 0 : _d.nodes) { if ((_d = app$1.graph) == null ? void 0 : _d.nodes) {
for (const node of app.graph.nodes) { for (const node of app$1.graph.nodes) {
const textWidget = (_e2 = node.widgets) == null ? void 0 : _e2.find( const textWidget = (_e2 = node.widgets) == null ? void 0 : _e2.find(
(w2) => w2.type === "AUTOCOMPLETE_TEXT_LORAS" (w2) => w2.type === "AUTOCOMPLETE_TEXT_LORAS"
); );
@@ -16568,7 +16731,7 @@ const initVueDomModeListener = () => {
const grid = nodeEl.querySelector('[data-testid="node-widgets"]'); const grid = nodeEl.querySelector('[data-testid="node-widgets"]');
if (!grid) continue; if (!grid) continue;
const nodeId = nodeEl.getAttribute("data-node-id"); const nodeId = nodeEl.getAttribute("data-node-id");
const node = (_a4 = app.graph) == null ? void 0 : _a4.getNodeById(nodeId); const node = (_a4 = app$1.graph) == null ? void 0 : _a4.getNodeById(nodeId);
if (!node) continue; if (!node) continue;
const rows = []; const rows = [];
let needsFix = false; let needsFix = false;
@@ -16589,7 +16752,7 @@ const initVueDomModeListener = () => {
} }
} }
}); });
(_f = app.canvas) == null ? void 0 : _f.setDirty(true, true); (_f = app$1.canvas) == null ? void 0 : _f.setDirty(true, true);
document.dispatchEvent(new CustomEvent("lora-manager:vue-mode-change", { document.dispatchEvent(new CustomEvent("lora-manager:vue-mode-change", {
detail: { isVueDomMode } detail: { isVueDomMode }
})); }));
@@ -16597,12 +16760,12 @@ const initVueDomModeListener = () => {
}); });
} }
}; };
if ((_a = app.ui) == null ? void 0 : _a.settings) { if ((_a = app$1.ui) == null ? void 0 : _a.settings) {
initVueDomModeListener(); initVueDomModeListener();
} else { } else {
const checkAppReady = setInterval(() => { const checkAppReady = setInterval(() => {
var _a2; var _a2;
if ((_a2 = app.ui) == null ? void 0 : _a2.settings) { if ((_a2 = app$1.ui) == null ? void 0 : _a2.settings) {
initVueDomModeListener(); initVueDomModeListener();
clearInterval(checkAppReady); clearInterval(checkAppReady);
} }
@@ -16641,8 +16804,8 @@ function createLoraInfoWidget(node) {
const vueApp = createApp(LoraInfoWidget, { const vueApp = createApp(LoraInfoWidget, {
widget, widget,
node, node,
api, api: api$1,
app, app: app$1,
isVueMode: typeof LiteGraph !== "undefined" && LiteGraph.vueNodesMode isVueMode: typeof LiteGraph !== "undefined" && LiteGraph.vueNodesMode
}); });
vueApp.use(PrimeVue, { vueApp.use(PrimeVue, {
@@ -16737,7 +16900,7 @@ function createAutocompleteTextWidgetFactory(node, widgetName, modelType, inputO
} }
); );
widget.metadataWidget = metadataWidget; widget.metadataWidget = metadataWidget;
const spellcheck = ((_c = (_b = (_a2 = app.ui) == null ? void 0 : _a2.settings) == null ? void 0 : _b.getSettingValue) == null ? void 0 : _c.call(_b, "Comfy.TextareaWidget.Spellcheck")) ?? false; const spellcheck = ((_c = (_b = (_a2 = app$1.ui) == null ? void 0 : _a2.settings) == null ? void 0 : _b.getSettingValue) == null ? void 0 : _c.call(_b, "Comfy.TextareaWidget.Spellcheck")) ?? false;
const maxHeight = modelType === "loras" ? AUTOCOMPLETE_TEXT_WIDGET_MAX_HEIGHT : void 0; const maxHeight = modelType === "loras" ? AUTOCOMPLETE_TEXT_WIDGET_MAX_HEIGHT : void 0;
const vueApp = createApp(AutocompleteTextWidget, { const vueApp = createApp(AutocompleteTextWidget, {
widget, widget,
@@ -16775,7 +16938,7 @@ function createAutocompleteTextWidgetFactory(node, widgetName, modelType, inputO
const minHeight = modelType === "loras" ? void 0 : AUTOCOMPLETE_TEXT_MIN_HEIGHT_DEFAULT; const minHeight = modelType === "loras" ? void 0 : AUTOCOMPLETE_TEXT_MIN_HEIGHT_DEFAULT;
return { widget, minWidth, minHeight }; return { widget, minWidth, minHeight };
} }
app.registerExtension({ app$1.registerExtension({
name: "LoraManager.VueWidgets", name: "LoraManager.VueWidgets",
getCustomWidgets() { getCustomWidgets() {
return { return {
File diff suppressed because one or more lines are too long