mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-20 18:51:26 -03:00
feat(ui): improve /activefilters discoverability on loras nodes
Mirror the /noautocomplete discoverability pattern for the loras\nactive-filters search toggle:\n\n- autocomplete.js: extend the slash-command-list footer and the\n one-time first-run hint to loras nodes, advertising\n /activefilters and /noactivefilters\n- lora_loader.js: add an 'Active Filters Search: ON/OFF' entry to the\n right-click menu of all loras-autocomplete node classes\n- settings.js: broadcast a 'lora-manager:setting-toggled' window event\n on every setLoraManagerSettingValue write\n- AutocompleteTextWidget.vue: add a persistent filter indicator chip\n (loras mode only) that reflects and toggles the setting and stays in\n sync via the setting-toggled event\n- tests: footer/hint/event coverage, context-menu tests for all four\n node classes, widget indicator tests; rebuild vue-widgets bundle
This commit is contained in:
@@ -133,4 +133,157 @@ describe('AutoComplete active-filters flag', () => {
|
|||||||
expect(call[0]).not.toContain('base_model=');
|
expect(call[0]).not.toContain('base_model=');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const typeLorasSlashCommand = async () => {
|
||||||
|
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 }));
|
||||||
|
return autoComplete;
|
||||||
|
};
|
||||||
|
|
||||||
|
it('shows the active-filters state below the loras slash command list', async () => {
|
||||||
|
await typeLorasSlashCommand();
|
||||||
|
|
||||||
|
const footer = document.querySelector('.lm-autocomplete-command-footer');
|
||||||
|
expect(footer).not.toBeNull();
|
||||||
|
expect(footer.textContent).toContain('Active Filters Search: OFF');
|
||||||
|
expect(footer.textContent).toContain('/activefilters to enable');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows how to disable active-filters search in the footer when it is on', async () => {
|
||||||
|
settingGetMock.mockImplementation((key) => {
|
||||||
|
if (key === 'loramanager.lora_active_filters_autocomplete') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
await typeLorasSlashCommand();
|
||||||
|
|
||||||
|
const footer = document.querySelector('.lm-autocomplete-command-footer');
|
||||||
|
expect(footer).not.toBeNull();
|
||||||
|
expect(footer.textContent).toContain('Active Filters Search: ON');
|
||||||
|
expect(footer.textContent).toContain('/noactivefilters to disable');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows a dismissible first-run hint on loras suggestions and remembers dismissal', async () => {
|
||||||
|
fetchApiMock.mockResolvedValue({
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
success: true,
|
||||||
|
relative_paths: ['models/example.safetensors'],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const triggerSearch = async () => {
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
await vi.runOnlyPendingTimersAsync();
|
||||||
|
await vi.runOnlyPendingTimersAsync();
|
||||||
|
await Promise.resolve();
|
||||||
|
return autoComplete;
|
||||||
|
};
|
||||||
|
|
||||||
|
const autoComplete = await triggerSearch();
|
||||||
|
const hint = autoComplete.dropdown.querySelector('.lm-autocomplete-first-run-hint');
|
||||||
|
expect(hint).not.toBeNull();
|
||||||
|
expect(hint.textContent).toContain('/activefilters');
|
||||||
|
|
||||||
|
hint.querySelector('button').click();
|
||||||
|
expect(autoComplete.dropdown.querySelector('.lm-autocomplete-first-run-hint')).toBeNull();
|
||||||
|
expect(localStorage.getItem('lm:activefilters-tip-dismissed')).toBe('1');
|
||||||
|
// A fresh instance no longer shows the hint once dismissed
|
||||||
|
const autoComplete2 = await triggerSearch();
|
||||||
|
expect(autoComplete2.dropdown.querySelector('.lm-autocomplete-first-run-hint')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not show the loras first-run hint when active-filters search is already on', async () => {
|
||||||
|
settingGetMock.mockImplementation((key) => {
|
||||||
|
if (key === 'loramanager.lora_active_filters_autocomplete') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
await vi.runOnlyPendingTimersAsync();
|
||||||
|
await vi.runOnlyPendingTimersAsync();
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||||
|
|
||||||
|
const {
|
||||||
|
APP_MODULE,
|
||||||
|
API_MODULE,
|
||||||
|
UTILS_MODULE,
|
||||||
|
SETTINGS_MODULE,
|
||||||
|
LORA_LOADER_MODULE,
|
||||||
|
} = vi.hoisted(() => ({
|
||||||
|
APP_MODULE: new URL("../../../scripts/app.js", import.meta.url).pathname,
|
||||||
|
API_MODULE: new URL("../../../scripts/api.js", import.meta.url).pathname,
|
||||||
|
UTILS_MODULE: new URL("../../../web/comfyui/utils.js", import.meta.url).pathname,
|
||||||
|
SETTINGS_MODULE: new URL("../../../web/comfyui/settings.js", import.meta.url).pathname,
|
||||||
|
LORA_LOADER_MODULE: new URL("../../../web/comfyui/lora_loader.js", import.meta.url).pathname,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const extensionState = { current: null };
|
||||||
|
const registerExtensionMock = vi.fn((extension) => {
|
||||||
|
extensionState.current = extension;
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock(APP_MODULE, () => ({
|
||||||
|
app: {
|
||||||
|
registerExtension: registerExtensionMock,
|
||||||
|
graph: {},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock(API_MODULE, () => ({
|
||||||
|
api: {
|
||||||
|
addEventListener: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const showToastMock = vi.fn();
|
||||||
|
|
||||||
|
vi.mock(UTILS_MODULE, () => ({
|
||||||
|
collectActiveLorasFromChain: vi.fn(),
|
||||||
|
updateConnectedTriggerWords: vi.fn(),
|
||||||
|
mergeLoras: vi.fn(),
|
||||||
|
chainCallback: (proto, property, callback) => {
|
||||||
|
proto[property] = callback;
|
||||||
|
},
|
||||||
|
getAllGraphNodes: vi.fn(),
|
||||||
|
getNodeFromGraph: vi.fn(),
|
||||||
|
getWidgetByName: vi.fn(),
|
||||||
|
getWidgetSerializedValue: vi.fn(),
|
||||||
|
showToast: showToastMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const getActiveFiltersPreferenceMock = vi.fn();
|
||||||
|
const setSettingValueMock = vi.fn();
|
||||||
|
|
||||||
|
vi.mock(SETTINGS_MODULE, () => ({
|
||||||
|
LORA_ACTIVE_FILTERS_AUTOCOMPLETE_SETTING_ID:
|
||||||
|
"loramanager.lora_active_filters_autocomplete",
|
||||||
|
SETTING_TOGGLED_EVENT_NAME: "lora-manager:setting-toggled",
|
||||||
|
getLoraActiveFiltersAutocompletePreference: getActiveFiltersPreferenceMock,
|
||||||
|
setLoraManagerSettingValue: setSettingValueMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
async function registerNodeType(comfyClass) {
|
||||||
|
await import(LORA_LOADER_MODULE);
|
||||||
|
const extension = extensionState.current;
|
||||||
|
expect(extension).toBeDefined();
|
||||||
|
const nodeType = { comfyClass, prototype: {} };
|
||||||
|
await extension.beforeRegisterNodeDef(nodeType, {}, {});
|
||||||
|
return nodeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMenuOption(nodeType, enabled) {
|
||||||
|
getActiveFiltersPreferenceMock.mockReturnValue(enabled);
|
||||||
|
const options = [];
|
||||||
|
nodeType.prototype.getExtraMenuOptions(null, options);
|
||||||
|
return options.find(
|
||||||
|
(option) =>
|
||||||
|
option &&
|
||||||
|
typeof option.content === "string" &&
|
||||||
|
option.content.startsWith("Active Filters Search:")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Lora Loader active-filters context menu", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.resetModules();
|
||||||
|
extensionState.current = null;
|
||||||
|
registerExtensionMock.mockClear();
|
||||||
|
showToastMock.mockClear();
|
||||||
|
getActiveFiltersPreferenceMock.mockReset();
|
||||||
|
setSettingValueMock.mockReset();
|
||||||
|
setSettingValueMock.mockResolvedValue(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
"Lora Loader (LoraManager)",
|
||||||
|
"Lora Stacker (LoraManager)",
|
||||||
|
"WanVideo Lora Select (LoraManager)",
|
||||||
|
"Create Hook LoRA (LoraManager)",
|
||||||
|
])("adds the toggle entry to the %s context menu", async (comfyClass) => {
|
||||||
|
const nodeType = await registerNodeType(comfyClass);
|
||||||
|
|
||||||
|
const option = getMenuOption(nodeType, false);
|
||||||
|
expect(option).toBeDefined();
|
||||||
|
expect(option.content).toContain("Active Filters Search: OFF");
|
||||||
|
expect(option.content).toContain("/activefilters to enable");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows the disable hint when active-filters search is on", async () => {
|
||||||
|
const nodeType = await registerNodeType("Lora Loader (LoraManager)");
|
||||||
|
|
||||||
|
const option = getMenuOption(nodeType, true);
|
||||||
|
expect(option.content).toContain("Active Filters Search: ON");
|
||||||
|
expect(option.content).toContain("/noactivefilters to disable");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("toggles the setting and toasts feedback", async () => {
|
||||||
|
const nodeType = await registerNodeType("Lora Loader (LoraManager)");
|
||||||
|
|
||||||
|
const enableOption = getMenuOption(nodeType, false);
|
||||||
|
await enableOption.callback();
|
||||||
|
|
||||||
|
expect(setSettingValueMock).toHaveBeenCalledWith(
|
||||||
|
"loramanager.lora_active_filters_autocomplete",
|
||||||
|
true
|
||||||
|
);
|
||||||
|
expect(showToastMock).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ summary: "Active Filters Search Enabled" })
|
||||||
|
);
|
||||||
|
|
||||||
|
const disableOption = getMenuOption(nodeType, true);
|
||||||
|
await disableOption.callback();
|
||||||
|
|
||||||
|
expect(setSettingValueMock).toHaveBeenCalledWith(
|
||||||
|
"loramanager.lora_active_filters_autocomplete",
|
||||||
|
false
|
||||||
|
);
|
||||||
|
expect(showToastMock).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ summary: "Active Filters Search Disabled" })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -23,6 +23,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>
|
||||||
@@ -30,6 +42,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
|
||||||
@@ -67,6 +81,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,
|
||||||
@@ -252,6 +308,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)
|
||||||
|
|
||||||
// Listen for custom event dispatched by main.ts
|
// Listen for custom event dispatched by main.ts
|
||||||
document.addEventListener('lora-manager:vue-mode-change', onModeChange)
|
document.addEventListener('lora-manager:vue-mode-change', onModeChange)
|
||||||
})
|
})
|
||||||
@@ -277,6 +338,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>
|
||||||
|
|
||||||
@@ -369,6 +431,58 @@ onUnmounted(() => {
|
|||||||
height: 12px;
|
height: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Active-filters search indicator (loras nodes only) */
|
||||||
|
.active-filters-toggle {
|
||||||
|
position: absolute;
|
||||||
|
top: 3px;
|
||||||
|
right: 3px;
|
||||||
|
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: 8px;
|
||||||
|
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: 8px;
|
right: 8px;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
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() {
|
||||||
@@ -134,3 +134,109 @@ describe('AutocompleteTextWidget clear button', () => {
|
|||||||
expect(widget.callback).toHaveBeenLastCalledWith('hello world')
|
expect(widget.callback).toHaveBeenLastCalledWith('hello world')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
+48
-16
@@ -23,6 +23,8 @@ import { showToast } from "./utils.js";
|
|||||||
|
|
||||||
// localStorage key for the one-time "how to disable" hint in the dropdown
|
// localStorage key for the one-time "how to disable" hint in the dropdown
|
||||||
const FIRST_RUN_HINT_DISMISSED_KEY = 'lm:autocomplete-disable-tip-dismissed';
|
const FIRST_RUN_HINT_DISMISSED_KEY = 'lm:autocomplete-disable-tip-dismissed';
|
||||||
|
// localStorage key for the one-time "try active filters search" hint (loras nodes)
|
||||||
|
const ACTIVE_FILTERS_HINT_DISMISSED_KEY = 'lm:activefilters-tip-dismissed';
|
||||||
|
|
||||||
// Command definitions for category filtering
|
// Command definitions for category filtering
|
||||||
const TAG_COMMANDS = {
|
const TAG_COMMANDS = {
|
||||||
@@ -1763,22 +1765,31 @@ class AutoComplete {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render a state hint below the slash command list so the autocomplete
|
* Render a state hint below the slash command list so the toggle commands
|
||||||
* toggle commands explain themselves. Only applies to prompt nodes.
|
* explain themselves. Prompt nodes advertise /autocomplete, loras nodes
|
||||||
|
* advertise /activefilters.
|
||||||
*/
|
*/
|
||||||
_renderCommandListFooter() {
|
_renderCommandListFooter() {
|
||||||
this._removeCommandListFooter();
|
this._removeCommandListFooter();
|
||||||
|
|
||||||
if (this.modelType !== 'prompt') {
|
let text = null;
|
||||||
|
if (this.modelType === 'prompt') {
|
||||||
|
const enabled = getPromptTagAutocompletePreference();
|
||||||
|
text = enabled
|
||||||
|
? 'Tag autocomplete is ON — /noautocomplete to disable'
|
||||||
|
: 'Tag autocomplete is OFF — /autocomplete to enable';
|
||||||
|
} else if (this.modelType === 'loras') {
|
||||||
|
const enabled = getLoraActiveFiltersAutocompletePreference();
|
||||||
|
text = enabled
|
||||||
|
? 'Active Filters Search: ON — /noactivefilters to disable'
|
||||||
|
: 'Active Filters Search: OFF — /activefilters to enable';
|
||||||
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const enabled = getPromptTagAutocompletePreference();
|
|
||||||
const footer = document.createElement('div');
|
const footer = document.createElement('div');
|
||||||
footer.className = 'lm-autocomplete-command-footer';
|
footer.className = 'lm-autocomplete-command-footer';
|
||||||
footer.textContent = enabled
|
footer.textContent = text;
|
||||||
? 'Tag autocomplete is ON — /noautocomplete to disable'
|
|
||||||
: 'Tag autocomplete is OFF — /autocomplete to enable';
|
|
||||||
footer.style.cssText = `
|
footer.style.cssText = `
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
@@ -1801,23 +1812,44 @@ class AutoComplete {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show a one-time, dismissible hint inside the dropdown telling users how
|
* Show a one-time, dismissible hint inside the dropdown surfacing the
|
||||||
* to disable tag autocomplete. Dismissal is persisted in localStorage.
|
* toggle commands: prompt nodes advertise /noautocomplete, loras nodes
|
||||||
|
* advertise /activefilters. Dismissal is persisted in localStorage.
|
||||||
*/
|
*/
|
||||||
_maybeShowFirstRunHint() {
|
_maybeShowFirstRunHint() {
|
||||||
if (this.firstRunHint) {
|
if (this.firstRunHint) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.modelType !== 'prompt'
|
|
||||||
|| this.showingCommands
|
let hintText = null;
|
||||||
|| this.searchType !== 'custom_words'
|
let storageKey = null;
|
||||||
|| this.activeCommand) {
|
|
||||||
|
if (this.modelType === 'prompt') {
|
||||||
|
// Only hint during plain tag searches, not command/embedding modes
|
||||||
|
if (this.showingCommands
|
||||||
|
|| this.searchType !== 'custom_words'
|
||||||
|
|| this.activeCommand) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hintText = 'Tip: type /noautocomplete to turn off these suggestions';
|
||||||
|
storageKey = FIRST_RUN_HINT_DISMISSED_KEY;
|
||||||
|
} else if (this.modelType === 'loras') {
|
||||||
|
// Only advertise active-filters search while it is disabled
|
||||||
|
if (this.showingCommands || this.activeCommand) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (getLoraActiveFiltersAutocompletePreference()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hintText = 'Tip: type /activefilters to search within the LoRA Manager page filters';
|
||||||
|
storageKey = ACTIVE_FILTERS_HINT_DISMISSED_KEY;
|
||||||
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let dismissed = false;
|
let dismissed = false;
|
||||||
try {
|
try {
|
||||||
dismissed = localStorage.getItem(FIRST_RUN_HINT_DISMISSED_KEY) === '1';
|
dismissed = localStorage.getItem(storageKey) === '1';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// localStorage unavailable - fall through and show the hint
|
// localStorage unavailable - fall through and show the hint
|
||||||
}
|
}
|
||||||
@@ -1839,7 +1871,7 @@ class AutoComplete {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const text = document.createElement('span');
|
const text = document.createElement('span');
|
||||||
text.textContent = 'Tip: type /noautocomplete to turn off these suggestions';
|
text.textContent = hintText;
|
||||||
|
|
||||||
const closeBtn = document.createElement('button');
|
const closeBtn = document.createElement('button');
|
||||||
closeBtn.type = 'button';
|
closeBtn.type = 'button';
|
||||||
@@ -1856,7 +1888,7 @@ class AutoComplete {
|
|||||||
`;
|
`;
|
||||||
closeBtn.addEventListener('click', () => {
|
closeBtn.addEventListener('click', () => {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(FIRST_RUN_HINT_DISMISSED_KEY, '1');
|
localStorage.setItem(storageKey, '1');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
}
|
}
|
||||||
this._removeFirstRunHint();
|
this._removeFirstRunHint();
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { app } from "../../scripts/app.js";
|
import { app } from "../../scripts/app.js";
|
||||||
import { api } from "../../scripts/api.js";
|
import { api } from "../../scripts/api.js";
|
||||||
|
import {
|
||||||
|
LORA_ACTIVE_FILTERS_AUTOCOMPLETE_SETTING_ID,
|
||||||
|
getLoraActiveFiltersAutocompletePreference,
|
||||||
|
setLoraManagerSettingValue,
|
||||||
|
} from "./settings.js";
|
||||||
|
import { showToast } from "./utils.js";
|
||||||
import {
|
import {
|
||||||
collectActiveLorasFromChain,
|
collectActiveLorasFromChain,
|
||||||
updateConnectedTriggerWords,
|
updateConnectedTriggerWords,
|
||||||
@@ -12,6 +18,65 @@ import {
|
|||||||
} from "./utils.js";
|
} from "./utils.js";
|
||||||
import { applyLoraValuesToText, debounce } from "./lora_syntax_utils.js";
|
import { applyLoraValuesToText, debounce } from "./lora_syntax_utils.js";
|
||||||
|
|
||||||
|
// Node classes whose "text" widget uses the loras autocomplete. Kept in sync
|
||||||
|
// with the broadcast-compatible classes in handleLoraCodeUpdate below.
|
||||||
|
const LORA_AUTOCOMPLETE_NODE_CLASSES = [
|
||||||
|
"Lora Loader (LoraManager)",
|
||||||
|
"Lora Stacker (LoraManager)",
|
||||||
|
"WanVideo Lora Select (LoraManager)",
|
||||||
|
"Create Hook LoRA (LoraManager)",
|
||||||
|
];
|
||||||
|
|
||||||
|
// Expose the active-filters search toggle in the node's right-click menu so
|
||||||
|
// users can discover the switch where the behavior actually happens, instead
|
||||||
|
// of only via the /activefilters and /noactivefilters slash commands. Mirrors
|
||||||
|
// the tag-autocomplete menu entry on Prompt (LoraManager) nodes.
|
||||||
|
function addActiveFiltersSearchMenuOption(nodeType) {
|
||||||
|
const getExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
|
||||||
|
nodeType.prototype.getExtraMenuOptions = function (_, options) {
|
||||||
|
getExtraMenuOptions?.apply?.(this, arguments);
|
||||||
|
|
||||||
|
options.push(null);
|
||||||
|
|
||||||
|
const filtersSearchEnabled = getLoraActiveFiltersAutocompletePreference();
|
||||||
|
options.push({
|
||||||
|
content: filtersSearchEnabled
|
||||||
|
? "Active Filters Search: ON (/noactivefilters to disable)"
|
||||||
|
: "Active Filters Search: OFF (/activefilters to enable)",
|
||||||
|
callback: async () => {
|
||||||
|
const newValue = !filtersSearchEnabled;
|
||||||
|
try {
|
||||||
|
const success = await setLoraManagerSettingValue(
|
||||||
|
LORA_ACTIVE_FILTERS_AUTOCOMPLETE_SETTING_ID,
|
||||||
|
newValue
|
||||||
|
);
|
||||||
|
if (!success) {
|
||||||
|
throw new Error("settings API unavailable");
|
||||||
|
}
|
||||||
|
showToast({
|
||||||
|
severity: newValue ? "success" : "secondary",
|
||||||
|
summary: newValue
|
||||||
|
? "Active Filters Search Enabled"
|
||||||
|
: "Active Filters Search Disabled",
|
||||||
|
detail: newValue
|
||||||
|
? "LoRA autocomplete now respects the active filters of the LoRA Manager page. Type /noactivefilters in the LoRA field to disable."
|
||||||
|
: "LoRA autocomplete searches the full library again. Type /activefilters in the LoRA field to re-enable.",
|
||||||
|
life: 3000,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[Lora Manager] Failed to toggle setting:", error);
|
||||||
|
showToast({
|
||||||
|
severity: "error",
|
||||||
|
summary: "Error",
|
||||||
|
detail: "Failed to toggle active filters search setting",
|
||||||
|
life: 3000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
app.registerExtension({
|
app.registerExtension({
|
||||||
name: "LoraManager.LoraLoader",
|
name: "LoraManager.LoraLoader",
|
||||||
|
|
||||||
@@ -35,12 +100,7 @@ app.registerExtension({
|
|||||||
// Handle broadcast mode (for Desktop/non-browser support)
|
// Handle broadcast mode (for Desktop/non-browser support)
|
||||||
if (numericNodeId === -1) {
|
if (numericNodeId === -1) {
|
||||||
// Find all compatible nodes in the current graph
|
// Find all compatible nodes in the current graph
|
||||||
const compatibleClasses = new Set([
|
const compatibleClasses = new Set(LORA_AUTOCOMPLETE_NODE_CLASSES);
|
||||||
"Lora Loader (LoraManager)",
|
|
||||||
"Lora Stacker (LoraManager)",
|
|
||||||
"WanVideo Lora Select (LoraManager)",
|
|
||||||
"Create Hook LoRA (LoraManager)",
|
|
||||||
]);
|
|
||||||
const targetNodes = getAllGraphNodes(app.graph)
|
const targetNodes = getAllGraphNodes(app.graph)
|
||||||
.map(({ node }) => node)
|
.map(({ node }) => node)
|
||||||
.filter((node) => compatibleClasses.has(node?.comfyClass));
|
.filter((node) => compatibleClasses.has(node?.comfyClass));
|
||||||
@@ -108,6 +168,10 @@ app.registerExtension({
|
|||||||
},
|
},
|
||||||
|
|
||||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||||
|
if (LORA_AUTOCOMPLETE_NODE_CLASSES.includes(nodeType.comfyClass)) {
|
||||||
|
addActiveFiltersSearchMenuOption(nodeType);
|
||||||
|
}
|
||||||
|
|
||||||
if (nodeType.comfyClass == "Lora Loader (LoraManager)") {
|
if (nodeType.comfyClass == "Lora Loader (LoraManager)") {
|
||||||
chainCallback(nodeType.prototype, "onNodeCreated", function () {
|
chainCallback(nodeType.prototype, "onNodeCreated", function () {
|
||||||
// Enable widget serialization
|
// Enable widget serialization
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
@@ -597,6 +616,8 @@ app.registerExtension({
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
PROMPT_TAG_AUTOCOMPLETE_SETTING_ID,
|
PROMPT_TAG_AUTOCOMPLETE_SETTING_ID,
|
||||||
|
LORA_ACTIVE_FILTERS_AUTOCOMPLETE_SETTING_ID,
|
||||||
|
SETTING_TOGGLED_EVENT_NAME,
|
||||||
getWheelSensitivity,
|
getWheelSensitivity,
|
||||||
getAutoPathCorrectionPreference,
|
getAutoPathCorrectionPreference,
|
||||||
getAutocompleteAppendCommaPreference,
|
getAutocompleteAppendCommaPreference,
|
||||||
|
|||||||
@@ -2118,14 +2118,14 @@ to { transform: rotate(360deg);
|
|||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.autocomplete-text-widget[data-v-4e322fec] {
|
.autocomplete-text-widget[data-v-8b0d98f3] {
|
||||||
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-4e322fec] {
|
.input-wrapper[data-v-8b0d98f3] {
|
||||||
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-4e322fec] {
|
.text-input[data-v-8b0d98f3] {
|
||||||
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-4e322fec] {
|
.text-input.vue-dom-mode[data-v-8b0d98f3] {
|
||||||
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-4e322fec]:focus {
|
.text-input[data-v-8b0d98f3]:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Clear button styles */
|
/* Clear button styles */
|
||||||
.clear-button[data-v-4e322fec] {
|
.clear-button[data-v-8b0d98f3] {
|
||||||
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,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-4e322fec] {
|
.input-wrapper:hover .clear-button[data-v-8b0d98f3] {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
.clear-button[data-v-4e322fec]:hover {
|
.clear-button[data-v-8b0d98f3]:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
background: rgba(255, 100, 100, 0.8);
|
background: rgba(255, 100, 100, 0.8);
|
||||||
}
|
}
|
||||||
.clear-button svg[data-v-4e322fec] {
|
.clear-button svg[data-v-8b0d98f3] {
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Active-filters search indicator (loras nodes only) */
|
||||||
|
.active-filters-toggle[data-v-8b0d98f3] {
|
||||||
|
position: absolute;
|
||||||
|
top: 3px;
|
||||||
|
right: 3px;
|
||||||
|
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-8b0d98f3]:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background: rgba(128, 128, 128, 0.45);
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
}
|
||||||
|
.active-filters-toggle.is-active[data-v-8b0d98f3] {
|
||||||
|
background: rgba(59, 130, 246, 0.35);
|
||||||
|
color: #7db8ff;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.active-filters-toggle svg[data-v-8b0d98f3] {
|
||||||
|
width: 11px;
|
||||||
|
height: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Vue DOM mode adjustments for the indicator */
|
||||||
|
.text-input.vue-dom-mode ~ .active-filters-toggle[data-v-8b0d98f3] {
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
.text-input.vue-dom-mode ~ .active-filters-toggle svg[data-v-8b0d98f3] {
|
||||||
|
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-4e322fec] {
|
.text-input.vue-dom-mode ~ .clear-button[data-v-8b0d98f3] {
|
||||||
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-4e322fec]:hover {
|
.text-input.vue-dom-mode ~ .clear-button[data-v-8b0d98f3]:hover {
|
||||||
background: oklch(62% 0.18 25);
|
background: oklch(62% 0.18 25);
|
||||||
}
|
}
|
||||||
.text-input.vue-dom-mode ~ .clear-button svg[data-v-4e322fec] {
|
.text-input.vue-dom-mode ~ .clear-button svg[data-v-8b0d98f3] {
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
}
|
}
|
||||||
@@ -11053,7 +11101,7 @@ const EditButton = /* @__PURE__ */ _export_sfc(_sfc_main$o, [["__scopeId", "data
|
|||||||
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$h = { class: "section__content" };
|
const _hoisted_3$h = { class: "section__content" };
|
||||||
const _hoisted_4$f = {
|
const _hoisted_4$g = {
|
||||||
key: 0,
|
key: 0,
|
||||||
class: "section__placeholder"
|
class: "section__placeholder"
|
||||||
};
|
};
|
||||||
@@ -11083,7 +11131,7 @@ const _sfc_main$n = /* @__PURE__ */ defineComponent({
|
|||||||
})
|
})
|
||||||
]),
|
]),
|
||||||
createBaseVNode("div", _hoisted_3$h, [
|
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$g, " 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, {
|
||||||
key: name,
|
key: name,
|
||||||
@@ -11102,7 +11150,7 @@ const BaseModelSection = /* @__PURE__ */ _export_sfc(_sfc_main$n, [["__scopeId",
|
|||||||
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$g = { class: "section__column" };
|
const _hoisted_3$g = { class: "section__column" };
|
||||||
const _hoisted_4$e = { class: "section__column-header" };
|
const _hoisted_4$f = { 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 = {
|
||||||
key: 0,
|
key: 0,
|
||||||
@@ -11138,7 +11186,7 @@ const _sfc_main$m = /* @__PURE__ */ defineComponent({
|
|||||||
], -1)),
|
], -1)),
|
||||||
createBaseVNode("div", _hoisted_2$i, [
|
createBaseVNode("div", _hoisted_2$i, [
|
||||||
createBaseVNode("div", _hoisted_3$g, [
|
createBaseVNode("div", _hoisted_3$g, [
|
||||||
createBaseVNode("div", _hoisted_4$e, [
|
createBaseVNode("div", _hoisted_4$f, [
|
||||||
_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, {
|
||||||
onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("edit-include"))
|
onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("edit-include"))
|
||||||
@@ -11184,7 +11232,7 @@ const TagsSection = /* @__PURE__ */ _export_sfc(_sfc_main$m, [["__scopeId", "dat
|
|||||||
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$f = { class: "section__column" };
|
const _hoisted_3$f = { class: "section__column" };
|
||||||
const _hoisted_4$d = { class: "section__column-header" };
|
const _hoisted_4$e = { 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 = {
|
||||||
key: 0,
|
key: 0,
|
||||||
@@ -11232,7 +11280,7 @@ const _sfc_main$l = /* @__PURE__ */ defineComponent({
|
|||||||
], -1)),
|
], -1)),
|
||||||
createBaseVNode("div", _hoisted_2$h, [
|
createBaseVNode("div", _hoisted_2$h, [
|
||||||
createBaseVNode("div", _hoisted_3$f, [
|
createBaseVNode("div", _hoisted_3$f, [
|
||||||
createBaseVNode("div", _hoisted_4$d, [
|
createBaseVNode("div", _hoisted_4$e, [
|
||||||
_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", {
|
||||||
type: "button",
|
type: "button",
|
||||||
@@ -11300,7 +11348,7 @@ const FoldersSection = /* @__PURE__ */ _export_sfc(_sfc_main$l, [["__scopeId", "
|
|||||||
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$e = { class: "section__toggle" };
|
const _hoisted_3$e = { class: "section__toggle" };
|
||||||
const _hoisted_4$c = ["checked"];
|
const _hoisted_4$d = ["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" };
|
||||||
const _hoisted_7$8 = { class: "section__input-wrapper" };
|
const _hoisted_7$8 = { class: "section__input-wrapper" };
|
||||||
@@ -11360,7 +11408,7 @@ const _sfc_main$k = /* @__PURE__ */ defineComponent({
|
|||||||
type: "checkbox",
|
type: "checkbox",
|
||||||
checked: __props.useRegex,
|
checked: __props.useRegex,
|
||||||
onChange: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("update:useRegex", $event.target.checked))
|
onChange: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("update:useRegex", $event.target.checked))
|
||||||
}, null, 40, _hoisted_4$c),
|
}, null, 40, _hoisted_4$d),
|
||||||
_cache[3] || (_cache[3] = createBaseVNode("span", { class: "section__toggle-label" }, "Use Regex", -1))
|
_cache[3] || (_cache[3] = createBaseVNode("span", { class: "section__toggle-label" }, "Use Regex", -1))
|
||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
@@ -11440,7 +11488,7 @@ const NamePatternsSection = /* @__PURE__ */ _export_sfc(_sfc_main$k, [["__scopeI
|
|||||||
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$d = { class: "toggle-item" };
|
const _hoisted_3$d = { class: "toggle-item" };
|
||||||
const _hoisted_4$b = ["aria-checked"];
|
const _hoisted_4$c = ["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"];
|
||||||
const _sfc_main$j = /* @__PURE__ */ defineComponent({
|
const _sfc_main$j = /* @__PURE__ */ defineComponent({
|
||||||
@@ -11471,7 +11519,7 @@ const _sfc_main$j = /* @__PURE__ */ defineComponent({
|
|||||||
}, [..._cache[2] || (_cache[2] = [
|
}, [..._cache[2] || (_cache[2] = [
|
||||||
createBaseVNode("span", { class: "toggle-switch__track" }, null, -1),
|
createBaseVNode("span", { class: "toggle-switch__track" }, null, -1),
|
||||||
createBaseVNode("span", { class: "toggle-switch__thumb" }, null, -1)
|
createBaseVNode("span", { class: "toggle-switch__thumb" }, null, -1)
|
||||||
])], 10, _hoisted_4$b)
|
])], 10, _hoisted_4$c)
|
||||||
]),
|
]),
|
||||||
createBaseVNode("label", _hoisted_5$9, [
|
createBaseVNode("label", _hoisted_5$9, [
|
||||||
_cache[5] || (_cache[5] = createBaseVNode("span", {
|
_cache[5] || (_cache[5] = createBaseVNode("span", {
|
||||||
@@ -11498,7 +11546,7 @@ const LicenseSection = /* @__PURE__ */ _export_sfc(_sfc_main$j, [["__scopeId", "
|
|||||||
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$c = ["disabled"];
|
const _hoisted_3$c = ["disabled"];
|
||||||
const _hoisted_4$a = {
|
const _hoisted_4$b = {
|
||||||
key: 0,
|
key: 0,
|
||||||
class: "preview__tooltip"
|
class: "preview__tooltip"
|
||||||
};
|
};
|
||||||
@@ -11560,7 +11608,7 @@ const _sfc_main$i = /* @__PURE__ */ defineComponent({
|
|||||||
], 32),
|
], 32),
|
||||||
createVNode(Transition, { name: "tooltip" }, {
|
createVNode(Transition, { name: "tooltip" }, {
|
||||||
default: withCtx(() => [
|
default: withCtx(() => [
|
||||||
showTooltip.value && __props.items.length > 0 ? (openBlock(), createElementBlock("div", _hoisted_4$a, [
|
showTooltip.value && __props.items.length > 0 ? (openBlock(), createElementBlock("div", _hoisted_4$b, [
|
||||||
createBaseVNode("div", _hoisted_5$8, [
|
createBaseVNode("div", _hoisted_5$8, [
|
||||||
(openBlock(true), createElementBlock(Fragment, null, renderList(__props.items.slice(0, 5), (item) => {
|
(openBlock(true), createElementBlock(Fragment, null, renderList(__props.items.slice(0, 5), (item) => {
|
||||||
return openBlock(), createElementBlock("div", {
|
return openBlock(), createElementBlock("div", {
|
||||||
@@ -11669,7 +11717,7 @@ const LoraPoolSummaryView = /* @__PURE__ */ _export_sfc(_sfc_main$h, [["__scopeI
|
|||||||
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$b = { class: "lora-pool-modal__title" };
|
const _hoisted_3$b = { class: "lora-pool-modal__title" };
|
||||||
const _hoisted_4$9 = {
|
const _hoisted_4$a = {
|
||||||
key: 0,
|
key: 0,
|
||||||
class: "lora-pool-modal__subtitle"
|
class: "lora-pool-modal__subtitle"
|
||||||
};
|
};
|
||||||
@@ -11729,7 +11777,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$b, 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$a, toDisplayString(__props.subtitle), 1)) : createCommentVNode("", true)
|
||||||
]),
|
]),
|
||||||
createBaseVNode("button", {
|
createBaseVNode("button", {
|
||||||
class: "lora-pool-modal__close",
|
class: "lora-pool-modal__close",
|
||||||
@@ -11757,7 +11805,7 @@ const ModalWrapper = /* @__PURE__ */ _export_sfc(_sfc_main$g, [["__scopeId", "da
|
|||||||
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$a = ["checked", "onChange"];
|
const _hoisted_3$a = ["checked", "onChange"];
|
||||||
const _hoisted_4$8 = { class: "model-checkbox-visual" };
|
const _hoisted_4$9 = { class: "model-checkbox-visual" };
|
||||||
const _hoisted_5$6 = {
|
const _hoisted_5$6 = {
|
||||||
key: 0,
|
key: 0,
|
||||||
class: "check-icon",
|
class: "check-icon",
|
||||||
@@ -11867,7 +11915,7 @@ const _sfc_main$f = /* @__PURE__ */ defineComponent({
|
|||||||
onChange: ($event) => toggleModel(model.name),
|
onChange: ($event) => toggleModel(model.name),
|
||||||
class: "model-checkbox"
|
class: "model-checkbox"
|
||||||
}, null, 40, _hoisted_3$a),
|
}, null, 40, _hoisted_3$a),
|
||||||
createBaseVNode("span", _hoisted_4$8, [
|
createBaseVNode("span", _hoisted_4$9, [
|
||||||
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)
|
||||||
])])) : createCommentVNode("", true)
|
])])) : createCommentVNode("", true)
|
||||||
@@ -11891,7 +11939,7 @@ const _hoisted_3$9 = {
|
|||||||
key: 0,
|
key: 0,
|
||||||
class: "no-results"
|
class: "no-results"
|
||||||
};
|
};
|
||||||
const _hoisted_4$7 = {
|
const _hoisted_4$8 = {
|
||||||
key: 1,
|
key: 1,
|
||||||
class: "load-more-hint"
|
class: "load-more-hint"
|
||||||
};
|
};
|
||||||
@@ -12032,7 +12080,7 @@ const _sfc_main$e = /* @__PURE__ */ defineComponent({
|
|||||||
}, 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$9, " 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$8, " Scroll to load more... ")) : createCommentVNode("", true)
|
||||||
], 544)
|
], 544)
|
||||||
]),
|
]),
|
||||||
_: 1
|
_: 1
|
||||||
@@ -12047,7 +12095,7 @@ const _hoisted_2$9 = {
|
|||||||
class: "tree-node__toggle-spacer"
|
class: "tree-node__toggle-spacer"
|
||||||
};
|
};
|
||||||
const _hoisted_3$8 = { class: "tree-node__checkbox-label" };
|
const _hoisted_3$8 = { class: "tree-node__checkbox-label" };
|
||||||
const _hoisted_4$6 = ["checked"];
|
const _hoisted_4$7 = ["checked"];
|
||||||
const _hoisted_5$5 = {
|
const _hoisted_5$5 = {
|
||||||
key: 0,
|
key: 0,
|
||||||
class: "tree-node__check-icon",
|
class: "tree-node__check-icon",
|
||||||
@@ -12113,7 +12161,7 @@ const _sfc_main$d = /* @__PURE__ */ defineComponent({
|
|||||||
class: "tree-node__checkbox",
|
class: "tree-node__checkbox",
|
||||||
checked: isSelected.value,
|
checked: isSelected.value,
|
||||||
onChange: _cache[1] || (_cache[1] = ($event) => _ctx.$emit("toggle-select", __props.node.key))
|
onChange: _cache[1] || (_cache[1] = ($event) => _ctx.$emit("toggle-select", __props.node.key))
|
||||||
}, null, 40, _hoisted_4$6),
|
}, null, 40, _hoisted_4$7),
|
||||||
createBaseVNode("span", {
|
createBaseVNode("span", {
|
||||||
class: normalizeClass(["tree-node__checkbox-visual", `tree-node__checkbox-visual--${__props.variant}`])
|
class: normalizeClass(["tree-node__checkbox-visual", `tree-node__checkbox-visual--${__props.variant}`])
|
||||||
}, [
|
}, [
|
||||||
@@ -12659,7 +12707,7 @@ const LoraPoolWidget = /* @__PURE__ */ _export_sfc(_sfc_main$b, [["__scopeId", "
|
|||||||
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$6 = ["src", "onError"];
|
const _hoisted_3$6 = ["src", "onError"];
|
||||||
const _hoisted_4$5 = {
|
const _hoisted_4$6 = {
|
||||||
key: 1,
|
key: 1,
|
||||||
class: "last-used-preview__thumb last-used-preview__thumb--placeholder"
|
class: "last-used-preview__thumb last-used-preview__thumb--placeholder"
|
||||||
};
|
};
|
||||||
@@ -12710,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$6)) : (openBlock(), createElementBlock("div", _hoisted_4$5, [..._cache[0] || (_cache[0] = [
|
}, null, 40, _hoisted_3$6)) : (openBlock(), createElementBlock("div", _hoisted_4$6, [..._cache[0] || (_cache[0] = [
|
||||||
createBaseVNode("svg", {
|
createBaseVNode("svg", {
|
||||||
viewBox: "0 0 16 16",
|
viewBox: "0 0 16 16",
|
||||||
fill: "currentColor"
|
fill: "currentColor"
|
||||||
@@ -13152,7 +13200,7 @@ const DualRangeSlider = /* @__PURE__ */ _export_sfc(_sfc_main$8, [["__scopeId",
|
|||||||
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$5 = { class: "count-mode-tabs" };
|
const _hoisted_3$5 = { class: "count-mode-tabs" };
|
||||||
const _hoisted_4$4 = ["checked"];
|
const _hoisted_4$5 = ["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" };
|
||||||
const _hoisted_7$3 = { class: "setting-section" };
|
const _hoisted_7$3 = { class: "setting-section" };
|
||||||
@@ -13226,7 +13274,7 @@ const _sfc_main$7 = /* @__PURE__ */ defineComponent({
|
|||||||
value: "fixed",
|
value: "fixed",
|
||||||
checked: __props.countMode === "fixed",
|
checked: __props.countMode === "fixed",
|
||||||
onChange: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("update:countMode", "fixed"))
|
onChange: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("update:countMode", "fixed"))
|
||||||
}, null, 40, _hoisted_4$4),
|
}, null, 40, _hoisted_4$5),
|
||||||
_cache[18] || (_cache[18] = createBaseVNode("span", { class: "count-mode-tab-label" }, "Fixed", -1))
|
_cache[18] || (_cache[18] = createBaseVNode("span", { class: "count-mode-tab-label" }, "Fixed", -1))
|
||||||
], 2),
|
], 2),
|
||||||
createBaseVNode("label", {
|
createBaseVNode("label", {
|
||||||
@@ -13848,7 +13896,7 @@ const LoraRandomizerWidget = /* @__PURE__ */ _export_sfc(_sfc_main$6, [["__scope
|
|||||||
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$4 = { class: "progress-label" };
|
const _hoisted_3$4 = { class: "progress-label" };
|
||||||
const _hoisted_4$3 = ["title"];
|
const _hoisted_4$4 = ["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" };
|
||||||
const _hoisted_7$2 = { class: "progress-total" };
|
const _hoisted_7$2 = { class: "progress-total" };
|
||||||
@@ -13985,7 +14033,7 @@ const _sfc_main$5 = /* @__PURE__ */ defineComponent({
|
|||||||
}, [
|
}, [
|
||||||
createBaseVNode("path", { d: "M7 10l5 5 5-5z" })
|
createBaseVNode("path", { d: "M7 10l5 5 5-5z" })
|
||||||
], -1))
|
], -1))
|
||||||
], 10, _hoisted_4$3)
|
], 10, _hoisted_4$4)
|
||||||
], 2),
|
], 2),
|
||||||
createBaseVNode("div", _hoisted_5$2, [
|
createBaseVNode("div", _hoisted_5$2, [
|
||||||
createBaseVNode("span", _hoisted_6$2, toDisplayString(__props.currentIndex), 1),
|
createBaseVNode("span", _hoisted_6$2, toDisplayString(__props.currentIndex), 1),
|
||||||
@@ -14173,7 +14221,7 @@ const LoraCyclerSettingsView = /* @__PURE__ */ _export_sfc(_sfc_main$5, [["__sco
|
|||||||
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$3 = ["onMouseenter", "onClick"];
|
const _hoisted_3$3 = ["onMouseenter", "onClick"];
|
||||||
const _hoisted_4$2 = { class: "lora-index" };
|
const _hoisted_4$3 = { class: "lora-index" };
|
||||||
const _hoisted_5$1 = ["title"];
|
const _hoisted_5$1 = ["title"];
|
||||||
const _hoisted_6$1 = {
|
const _hoisted_6$1 = {
|
||||||
key: 0,
|
key: 0,
|
||||||
@@ -14347,7 +14395,7 @@ const _sfc_main$4 = /* @__PURE__ */ defineComponent({
|
|||||||
onMouseleave: hidePreview,
|
onMouseleave: hidePreview,
|
||||||
onClick: ($event) => selectLora(item.index)
|
onClick: ($event) => selectLora(item.index)
|
||||||
}, [
|
}, [
|
||||||
createBaseVNode("span", _hoisted_4$2, toDisplayString(item.index), 1),
|
createBaseVNode("span", _hoisted_4$3, toDisplayString(item.index), 1),
|
||||||
createBaseVNode("span", {
|
createBaseVNode("span", {
|
||||||
class: "lora-name",
|
class: "lora-name",
|
||||||
title: item.lora.file_name
|
title: item.lora.file_name
|
||||||
@@ -14970,7 +15018,7 @@ const _hoisted_2$2 = {
|
|||||||
ref: "contentRef"
|
ref: "contentRef"
|
||||||
};
|
};
|
||||||
const _hoisted_3$2 = ["innerHTML"];
|
const _hoisted_3$2 = ["innerHTML"];
|
||||||
const _hoisted_4$1 = {
|
const _hoisted_4$2 = {
|
||||||
key: 1,
|
key: 1,
|
||||||
class: "placeholder"
|
class: "placeholder"
|
||||||
};
|
};
|
||||||
@@ -15064,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$2)) : (openBlock(), createElementBlock("div", _hoisted_4$1, "No metadata available"))
|
}, null, 8, _hoisted_3$2)) : (openBlock(), createElementBlock("div", _hoisted_4$2, "No metadata available"))
|
||||||
], 512)
|
], 512)
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
@@ -15136,9 +15184,72 @@ 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 = { class: "input-wrapper" };
|
const _hoisted_2$1 = { class: "input-wrapper" };
|
||||||
const _hoisted_3$1 = ["placeholder", "spellcheck"];
|
const _hoisted_3$1 = ["placeholder", "spellcheck"];
|
||||||
|
const _hoisted_4$1 = ["title"];
|
||||||
const _sfc_main$1 = /* @__PURE__ */ defineComponent({
|
const _sfc_main$1 = /* @__PURE__ */ defineComponent({
|
||||||
__name: "AutocompleteTextWidget",
|
__name: "AutocompleteTextWidget",
|
||||||
props: {
|
props: {
|
||||||
@@ -15160,6 +15271,37 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
|
|||||||
const textareaRef = ref(null);
|
const textareaRef = ref(null);
|
||||||
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",
|
||||||
@@ -15266,6 +15408,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);
|
||||||
document.addEventListener("lora-manager:vue-mode-change", onModeChange);
|
document.addEventListener("lora-manager:vue-mode-change", onModeChange);
|
||||||
});
|
});
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
@@ -15282,6 +15426,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, [
|
||||||
@@ -15323,13 +15468,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_4$1)) : createCommentVNode("", true)
|
||||||
])
|
])
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const AutocompleteTextWidget = /* @__PURE__ */ _export_sfc(_sfc_main$1, [["__scopeId", "data-v-4e322fec"]]);
|
const AutocompleteTextWidget = /* @__PURE__ */ _export_sfc(_sfc_main$1, [["__scopeId", "data-v-8b0d98f3"]]);
|
||||||
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" };
|
||||||
@@ -15746,7 +15907,6 @@ function createModeChangeCallback(node, updateDownstreamLoaders2, nodeSpecificCa
|
|||||||
updateDownstreamLoaders2(node);
|
updateDownstreamLoaders2(node);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const app = {};
|
|
||||||
const api = {
|
const api = {
|
||||||
fetchApi: (...args) => fetch(...args),
|
fetchApi: (...args) => fetch(...args),
|
||||||
addEventListener: (eventName, handler) => document.addEventListener(eventName, handler),
|
addEventListener: (eventName, handler) => document.addEventListener(eventName, handler),
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user