mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-08-24 14:31:26 -03:00
feat(ui): improve tag autocomplete toggle discoverability in prompt nodes
- Add Tag Autocomplete ON/OFF entry to the Prompt (LoraManager) node right-click menu, cross-referencing the slash commands - Show the current autocomplete state (/autocomplete or /noautocomplete hint) below the slash command list - Show a one-time dismissible tip in the suggestion dropdown on first use - Clarify toggle command labels (Turn autocomplete ON/OFF) and cross-link all three entry points in the settings tooltip - Share the setting write path via setLoraManagerSettingValue()
This commit is contained in:
@@ -2032,4 +2032,122 @@ describe('AutoComplete widget interactions', () => {
|
|||||||
expect(calledUrl).toContain('folder=Flux.1+D%2Fstyle');
|
expect(calledUrl).toContain('folder=Flux.1+D%2Fstyle');
|
||||||
expect(calledUrl).toContain('recursive=true');
|
expect(calledUrl).toContain('recursive=true');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('discoverability hints', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorage.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
const typeSlashCommand = 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, 'prompt', { showPreview: false, minChars: 1 });
|
||||||
|
|
||||||
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
return autoComplete;
|
||||||
|
};
|
||||||
|
|
||||||
|
it('shows the current autocomplete state below the slash command list', async () => {
|
||||||
|
const autoComplete = await typeSlashCommand();
|
||||||
|
|
||||||
|
const footer = autoComplete.dropdown.querySelector('.lm-autocomplete-command-footer');
|
||||||
|
expect(footer).not.toBeNull();
|
||||||
|
expect(footer.textContent).toContain('/noautocomplete to disable');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows how to re-enable autocomplete in the footer when it is off', async () => {
|
||||||
|
settingGetMock.mockImplementation((key) => {
|
||||||
|
if (key === 'loramanager.prompt_tag_autocomplete') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
const autoComplete = await typeSlashCommand();
|
||||||
|
|
||||||
|
const footer = autoComplete.dropdown.querySelector('.lm-autocomplete-command-footer');
|
||||||
|
expect(footer).not.toBeNull();
|
||||||
|
expect(footer.textContent).toContain('/autocomplete to enable');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stays silent when typing with tag autocomplete disabled', async () => {
|
||||||
|
settingGetMock.mockImplementation((key) => {
|
||||||
|
if (key === 'loramanager.prompt_tag_autocomplete') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (key === 'loramanager.autocomplete_accept_key') {
|
||||||
|
return 'both';
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
const input = document.createElement('textarea');
|
||||||
|
input.value = 'hello';
|
||||||
|
input.selectionStart = 5;
|
||||||
|
document.body.append(input);
|
||||||
|
|
||||||
|
caretHelperInstance.getBeforeCursor.mockReturnValue('hello');
|
||||||
|
|
||||||
|
const { AutoComplete } = await import(AUTOCOMPLETE_MODULE);
|
||||||
|
const autoComplete = new AutoComplete(input, 'prompt', { showPreview: false, minChars: 1 });
|
||||||
|
|
||||||
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
|
||||||
|
expect(autoComplete.isVisible).toBe(false);
|
||||||
|
expect(fetchApiMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows a dismissible first-run hint on tag suggestions and remembers dismissal', async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
|
||||||
|
fetchApiMock.mockResolvedValue({
|
||||||
|
json: () => Promise.resolve({
|
||||||
|
success: true,
|
||||||
|
words: [{ tag_name: '1girl', category: 4, post_count: 500000 }],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
caretHelperInstance.getBeforeCursor.mockReturnValue('1gi');
|
||||||
|
|
||||||
|
const triggerSearch = async () => {
|
||||||
|
const input = document.createElement('textarea');
|
||||||
|
input.value = '1gi';
|
||||||
|
input.selectionStart = 3;
|
||||||
|
document.body.append(input);
|
||||||
|
|
||||||
|
const { AutoComplete } = await import(AUTOCOMPLETE_MODULE);
|
||||||
|
const autoComplete = new AutoComplete(input, 'prompt', {
|
||||||
|
debounceDelay: 0,
|
||||||
|
showPreview: false,
|
||||||
|
minChars: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
await vi.runAllTimersAsync();
|
||||||
|
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('/noautocomplete');
|
||||||
|
|
||||||
|
hint.querySelector('button').click();
|
||||||
|
|
||||||
|
expect(autoComplete.dropdown.querySelector('.lm-autocomplete-first-run-hint')).toBeNull();
|
||||||
|
expect(localStorage.getItem('lm:autocomplete-disable-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();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+147
-19
@@ -17,9 +17,13 @@ import {
|
|||||||
getLoraActiveFiltersAutocompletePreference,
|
getLoraActiveFiltersAutocompletePreference,
|
||||||
getPromptTagAutocompletePreference,
|
getPromptTagAutocompletePreference,
|
||||||
getTagSpaceReplacementPreference,
|
getTagSpaceReplacementPreference,
|
||||||
|
setLoraManagerSettingValue,
|
||||||
} from "./settings.js";
|
} from "./settings.js";
|
||||||
import { showToast } from "./utils.js";
|
import { showToast } from "./utils.js";
|
||||||
|
|
||||||
|
// localStorage key for the one-time "how to disable" hint in the dropdown
|
||||||
|
const FIRST_RUN_HINT_DISMISSED_KEY = 'lm:autocomplete-disable-tip-dismissed';
|
||||||
|
|
||||||
// Command definitions for category filtering
|
// Command definitions for category filtering
|
||||||
const TAG_COMMANDS = {
|
const TAG_COMMANDS = {
|
||||||
'/character': { categories: [4, 11], label: 'Character' },
|
'/character': { categories: [4, 11], label: 'Character' },
|
||||||
@@ -37,14 +41,14 @@ const TAG_COMMANDS = {
|
|||||||
type: 'toggle_setting',
|
type: 'toggle_setting',
|
||||||
settingId: 'loramanager.prompt_tag_autocomplete',
|
settingId: 'loramanager.prompt_tag_autocomplete',
|
||||||
value: true,
|
value: true,
|
||||||
label: 'Autocomplete: ON',
|
label: 'Turn autocomplete ON',
|
||||||
condition: () => !getPromptTagAutocompletePreference()
|
condition: () => !getPromptTagAutocompletePreference()
|
||||||
},
|
},
|
||||||
'/noautocomplete': {
|
'/noautocomplete': {
|
||||||
type: 'toggle_setting',
|
type: 'toggle_setting',
|
||||||
settingId: 'loramanager.prompt_tag_autocomplete',
|
settingId: 'loramanager.prompt_tag_autocomplete',
|
||||||
value: false,
|
value: false,
|
||||||
label: 'Autocomplete: OFF',
|
label: 'Turn autocomplete OFF',
|
||||||
condition: () => getPromptTagAutocompletePreference()
|
condition: () => getPromptTagAutocompletePreference()
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -55,7 +59,7 @@ const LORAS_COMMANDS = {
|
|||||||
type: 'toggle_setting',
|
type: 'toggle_setting',
|
||||||
settingId: 'loramanager.lora_active_filters_autocomplete',
|
settingId: 'loramanager.lora_active_filters_autocomplete',
|
||||||
value: true,
|
value: true,
|
||||||
label: 'Active Filters: ON',
|
label: 'Turn active filters search ON',
|
||||||
feedbackSummary: 'Active Filters Search: ON',
|
feedbackSummary: 'Active Filters Search: ON',
|
||||||
feedbackDetail: 'LoRA autocomplete now searches within the active filters of the LoRA Manager page.',
|
feedbackDetail: 'LoRA autocomplete now searches within the active filters of the LoRA Manager page.',
|
||||||
condition: () => !getLoraActiveFiltersAutocompletePreference()
|
condition: () => !getLoraActiveFiltersAutocompletePreference()
|
||||||
@@ -64,7 +68,7 @@ const LORAS_COMMANDS = {
|
|||||||
type: 'toggle_setting',
|
type: 'toggle_setting',
|
||||||
settingId: 'loramanager.lora_active_filters_autocomplete',
|
settingId: 'loramanager.lora_active_filters_autocomplete',
|
||||||
value: false,
|
value: false,
|
||||||
label: 'Active Filters: OFF',
|
label: 'Turn active filters search OFF',
|
||||||
feedbackSummary: 'Active Filters Search: OFF',
|
feedbackSummary: 'Active Filters Search: OFF',
|
||||||
feedbackDetail: 'LoRA autocomplete searches the full library again.',
|
feedbackDetail: 'LoRA autocomplete searches the full library again.',
|
||||||
condition: () => getLoraActiveFiltersAutocompletePreference()
|
condition: () => getLoraActiveFiltersAutocompletePreference()
|
||||||
@@ -72,8 +76,7 @@ const LORAS_COMMANDS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Category display information
|
// Category display information
|
||||||
const CATEGORY_INFO = {
|
const CATEGORY_INFO = { 0: { bg: 'rgba(0, 155, 230, 0.2)', text: '#4bb4ff', label: 'General' },
|
||||||
0: { bg: 'rgba(0, 155, 230, 0.2)', text: '#4bb4ff', label: 'General' },
|
|
||||||
1: { bg: 'rgba(255, 138, 139, 0.2)', text: '#ffc3c3', label: 'Artist' },
|
1: { bg: 'rgba(255, 138, 139, 0.2)', text: '#ffc3c3', label: 'Artist' },
|
||||||
3: { bg: 'rgba(199, 151, 255, 0.2)', text: '#ddc9fb', label: 'Copyright' },
|
3: { bg: 'rgba(199, 151, 255, 0.2)', text: '#ddc9fb', label: 'Copyright' },
|
||||||
4: { bg: 'rgba(53, 198, 74, 0.2)', text: '#93e49a', label: 'Character' },
|
4: { bg: 'rgba(53, 198, 74, 0.2)', text: '#93e49a', label: 'Character' },
|
||||||
@@ -471,6 +474,10 @@ class AutoComplete {
|
|||||||
this.searchType = null;
|
this.searchType = null;
|
||||||
this.suppressAutocompleteOnce = false;
|
this.suppressAutocompleteOnce = false;
|
||||||
|
|
||||||
|
// Discoverability hints state
|
||||||
|
this.commandListFooter = null; // State hint shown below the slash command list
|
||||||
|
this.firstRunHint = null; // One-time "how to disable" bar inside the dropdown
|
||||||
|
|
||||||
// Virtual scrolling state
|
// Virtual scrolling state
|
||||||
this.virtualScrollOffset = 0;
|
this.virtualScrollOffset = 0;
|
||||||
this.hasMoreItems = true;
|
this.hasMoreItems = true;
|
||||||
@@ -829,7 +836,9 @@ class AutoComplete {
|
|||||||
searchTerm = rawSearchTerm;
|
searchTerm = rawSearchTerm;
|
||||||
this.searchType = 'custom_words';
|
this.searchType = 'custom_words';
|
||||||
} else {
|
} else {
|
||||||
// No command and setting disabled - no autocomplete for direct typing
|
// No command and setting disabled - no autocomplete for direct typing.
|
||||||
|
// Re-enable discovery is covered by the command-list footer,
|
||||||
|
// the node context menu and the settings tooltip.
|
||||||
this.hide();
|
this.hide();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1708,12 +1717,131 @@ class AutoComplete {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// State hint below the command list (e.g. how to toggle autocomplete)
|
||||||
|
this._renderCommandListFooter();
|
||||||
|
|
||||||
// Update virtual scroll height for virtual scrolling mode
|
// Update virtual scroll height for virtual scrolling mode
|
||||||
if (this.contentContainer) {
|
if (this.contentContainer) {
|
||||||
this.updateVirtualScrollHeight();
|
this.updateVirtualScrollHeight();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a state hint below the slash command list so the autocomplete
|
||||||
|
* toggle commands explain themselves. Only applies to prompt nodes.
|
||||||
|
*/
|
||||||
|
_renderCommandListFooter() {
|
||||||
|
this._removeCommandListFooter();
|
||||||
|
|
||||||
|
if (this.modelType !== 'prompt') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const enabled = getPromptTagAutocompletePreference();
|
||||||
|
const footer = document.createElement('div');
|
||||||
|
footer.className = 'lm-autocomplete-command-footer';
|
||||||
|
footer.textContent = enabled
|
||||||
|
? 'Tag autocomplete is ON — /noautocomplete to disable'
|
||||||
|
: 'Tag autocomplete is OFF — /autocomplete to enable';
|
||||||
|
footer.style.cssText = `
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(226, 232, 240, 0.5);
|
||||||
|
border-top: 1px solid rgba(226, 232, 240, 0.1);
|
||||||
|
white-space: nowrap;
|
||||||
|
`;
|
||||||
|
// Keep focus in the textarea when the hint is clicked
|
||||||
|
footer.addEventListener('mousedown', (e) => e.preventDefault());
|
||||||
|
|
||||||
|
this.dropdown.appendChild(footer);
|
||||||
|
this.commandListFooter = footer;
|
||||||
|
}
|
||||||
|
|
||||||
|
_removeCommandListFooter() {
|
||||||
|
if (this.commandListFooter) {
|
||||||
|
this.commandListFooter.remove();
|
||||||
|
this.commandListFooter = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a one-time, dismissible hint inside the dropdown telling users how
|
||||||
|
* to disable tag autocomplete. Dismissal is persisted in localStorage.
|
||||||
|
*/
|
||||||
|
_maybeShowFirstRunHint() {
|
||||||
|
if (this.firstRunHint) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.modelType !== 'prompt'
|
||||||
|
|| this.showingCommands
|
||||||
|
|| this.searchType !== 'custom_words'
|
||||||
|
|| this.activeCommand) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dismissed = false;
|
||||||
|
try {
|
||||||
|
dismissed = localStorage.getItem(FIRST_RUN_HINT_DISMISSED_KEY) === '1';
|
||||||
|
} catch (e) {
|
||||||
|
// localStorage unavailable - fall through and show the hint
|
||||||
|
}
|
||||||
|
if (dismissed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hint = document.createElement('div');
|
||||||
|
hint.className = 'lm-autocomplete-first-run-hint';
|
||||||
|
hint.style.cssText = `
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(226, 232, 240, 0.6);
|
||||||
|
border-bottom: 1px solid rgba(226, 232, 240, 0.1);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const text = document.createElement('span');
|
||||||
|
text.textContent = 'Tip: type /noautocomplete to turn off these suggestions';
|
||||||
|
|
||||||
|
const closeBtn = document.createElement('button');
|
||||||
|
closeBtn.type = 'button';
|
||||||
|
closeBtn.textContent = '×';
|
||||||
|
closeBtn.title = 'Dismiss';
|
||||||
|
closeBtn.style.cssText = `
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: rgba(226, 232, 240, 0.5);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0 2px;
|
||||||
|
`;
|
||||||
|
closeBtn.addEventListener('click', () => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(FIRST_RUN_HINT_DISMISSED_KEY, '1');
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
this._removeFirstRunHint();
|
||||||
|
});
|
||||||
|
|
||||||
|
hint.appendChild(text);
|
||||||
|
hint.appendChild(closeBtn);
|
||||||
|
// Keep focus in the textarea when interacting with the hint
|
||||||
|
hint.addEventListener('mousedown', (e) => e.preventDefault());
|
||||||
|
|
||||||
|
this.dropdown.insertBefore(hint, this.dropdown.firstChild);
|
||||||
|
this.firstRunHint = hint;
|
||||||
|
}
|
||||||
|
|
||||||
|
_removeFirstRunHint() {
|
||||||
|
if (this.firstRunHint) {
|
||||||
|
this.firstRunHint.remove();
|
||||||
|
this.firstRunHint = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Insert a command into the input
|
* Insert a command into the input
|
||||||
* @param {string} command - The command to insert (e.g., "/character")
|
* @param {string} command - The command to insert (e.g., "/character")
|
||||||
@@ -1745,6 +1873,9 @@ class AutoComplete {
|
|||||||
this.selectedIndex = -1;
|
this.selectedIndex = -1;
|
||||||
this.hasManualSelection = false;
|
this.hasManualSelection = false;
|
||||||
|
|
||||||
|
// Command-list state hints do not belong to regular search results
|
||||||
|
this._removeCommandListFooter();
|
||||||
|
|
||||||
// Reset virtual scroll state
|
// Reset virtual scroll state
|
||||||
this.virtualScrollOffset = 0;
|
this.virtualScrollOffset = 0;
|
||||||
this.currentPage = 0;
|
this.currentPage = 0;
|
||||||
@@ -2447,6 +2578,7 @@ class AutoComplete {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._maybeShowFirstRunHint();
|
||||||
// For virtual scrolling, render items first so positionAtCursor can measure width correctly
|
// For virtual scrolling, render items first so positionAtCursor can measure width correctly
|
||||||
if (this.options.enableVirtualScroll && this.contentContainer) {
|
if (this.options.enableVirtualScroll && this.contentContainer) {
|
||||||
this.dropdown.style.display = 'block';
|
this.dropdown.style.display = 'block';
|
||||||
@@ -2516,6 +2648,10 @@ class AutoComplete {
|
|||||||
this.hasManualSelection = false;
|
this.hasManualSelection = false;
|
||||||
this.showingCommands = false;
|
this.showingCommands = false;
|
||||||
|
|
||||||
|
// Remove discoverability hints attached to the dropdown
|
||||||
|
this._removeCommandListFooter();
|
||||||
|
this._removeFirstRunHint();
|
||||||
|
|
||||||
// Clear items to prevent stale data from being displayed
|
// Clear items to prevent stale data from being displayed
|
||||||
// when autocomplete is shown again
|
// when autocomplete is shown again
|
||||||
this.items = [];
|
this.items = [];
|
||||||
@@ -2854,20 +2990,12 @@ class AutoComplete {
|
|||||||
const { settingId, value } = command;
|
const { settingId, value } = command;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use ComfyUI's setting API to update global setting
|
const success = await setLoraManagerSettingValue(settingId, value);
|
||||||
const settingManager = app?.extensionManager?.setting;
|
if (success) {
|
||||||
if (settingManager && typeof settingManager.set === 'function') {
|
|
||||||
await settingManager.set(settingId, value);
|
|
||||||
this._showToggleFeedback(command, value);
|
this._showToggleFeedback(command, value);
|
||||||
this._clearCurrentToken();
|
this._clearCurrentToken();
|
||||||
} else {
|
} else {
|
||||||
// Fallback: use legacy settings API
|
throw new Error('settings API unavailable');
|
||||||
const setting = app.ui.settings.settingsById?.[settingId];
|
|
||||||
if (setting) {
|
|
||||||
app.ui.settings.setSettingValue(settingId, value);
|
|
||||||
this._showToggleFeedback(command, value);
|
|
||||||
this._clearCurrentToken();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Lora Manager] Failed to toggle setting:', error);
|
console.error('[Lora Manager] Failed to toggle setting:', error);
|
||||||
@@ -2893,7 +3021,7 @@ class AutoComplete {
|
|||||||
summary: command.feedbackSummary || (enabled ? 'Autocomplete Enabled' : 'Autocomplete Disabled'),
|
summary: command.feedbackSummary || (enabled ? 'Autocomplete Enabled' : 'Autocomplete Disabled'),
|
||||||
detail: command.feedbackDetail || (enabled
|
detail: command.feedbackDetail || (enabled
|
||||||
? 'Tag autocomplete is now ON. Type to see suggestions.'
|
? 'Tag autocomplete is now ON. Type to see suggestions.'
|
||||||
: 'Tag autocomplete is now OFF. Use /autocomplete to re-enable.'),
|
: 'Tag autocomplete is now OFF. Use /autocomplete or the node right-click menu to re-enable.'),
|
||||||
life: 3000
|
life: 3000
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { app } from "../../scripts/app.js";
|
import { app } from "../../scripts/app.js";
|
||||||
|
import {
|
||||||
|
PROMPT_TAG_AUTOCOMPLETE_SETTING_ID,
|
||||||
|
getPromptTagAutocompletePreference,
|
||||||
|
setLoraManagerSettingValue,
|
||||||
|
} from "./settings.js";
|
||||||
|
import { showToast } from "./utils.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extension for PromptLM node to support dynamic trigger_words inputs.
|
* Extension for PromptLM node to support dynamic trigger_words inputs.
|
||||||
@@ -93,6 +99,48 @@ app.registerExtension({
|
|||||||
|
|
||||||
return onConnectionsChange?.apply?.(this, arguments);
|
return onConnectionsChange?.apply?.(this, arguments);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Expose the tag autocomplete toggle in the node's right-click menu so
|
||||||
|
// users can discover the switch where the behavior actually happens,
|
||||||
|
// instead of only via slash commands or the global settings dialog.
|
||||||
|
const getExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
|
||||||
|
nodeType.prototype.getExtraMenuOptions = function(_, options) {
|
||||||
|
getExtraMenuOptions?.apply?.(this, arguments);
|
||||||
|
|
||||||
|
options.push(null);
|
||||||
|
|
||||||
|
const autocompleteEnabled = getPromptTagAutocompletePreference();
|
||||||
|
options.push({
|
||||||
|
content: autocompleteEnabled
|
||||||
|
? "Tag Autocomplete: ON (/noautocomplete to disable)"
|
||||||
|
: "Tag Autocomplete: OFF (/autocomplete to enable)",
|
||||||
|
callback: async () => {
|
||||||
|
const newValue = !autocompleteEnabled;
|
||||||
|
try {
|
||||||
|
const success = await setLoraManagerSettingValue(PROMPT_TAG_AUTOCOMPLETE_SETTING_ID, newValue);
|
||||||
|
if (!success) {
|
||||||
|
throw new Error("settings API unavailable");
|
||||||
|
}
|
||||||
|
showToast({
|
||||||
|
severity: newValue ? 'success' : 'secondary',
|
||||||
|
summary: newValue ? 'Autocomplete Enabled' : 'Autocomplete Disabled',
|
||||||
|
detail: newValue
|
||||||
|
? 'Tag autocomplete is now ON. Type to see suggestions.'
|
||||||
|
: 'Tag autocomplete is now OFF. Type /autocomplete in the prompt 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 autocomplete setting',
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
nodeCreated(node, app) {
|
nodeCreated(node, app) {
|
||||||
|
|||||||
+23
-1
@@ -172,6 +172,26 @@ const getPromptTagAutocompletePreference = (() => {
|
|||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persist a LoRA Manager setting through ComfyUI's setting API.
|
||||||
|
* Returns true when the setting was written successfully.
|
||||||
|
*/
|
||||||
|
const setLoraManagerSettingValue = async (settingId, value) => {
|
||||||
|
const settingManager = app?.extensionManager?.setting;
|
||||||
|
if (settingManager && typeof settingManager.set === "function") {
|
||||||
|
await settingManager.set(settingId, value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const setting = app?.ui?.settings?.settingsById?.[settingId];
|
||||||
|
if (setting) {
|
||||||
|
app.ui.settings.setSettingValue(settingId, value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
const getAutocompleteAppendCommaPreference = (() => {
|
const getAutocompleteAppendCommaPreference = (() => {
|
||||||
let settingsUnavailableLogged = false;
|
let settingsUnavailableLogged = false;
|
||||||
|
|
||||||
@@ -422,7 +442,7 @@ app.registerExtension({
|
|||||||
name: "Enable Tag Autocomplete in Prompt Nodes",
|
name: "Enable Tag Autocomplete in Prompt Nodes",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
defaultValue: PROMPT_TAG_AUTOCOMPLETE_DEFAULT,
|
defaultValue: PROMPT_TAG_AUTOCOMPLETE_DEFAULT,
|
||||||
tooltip: "When enabled, typing will trigger tag autocomplete suggestions. Commands (e.g., /character, /artist) always work regardless of this setting.",
|
tooltip: "When enabled, typing in a Prompt (LoraManager) node triggers tag autocomplete suggestions. You can also toggle it by typing /autocomplete or /noautocomplete in the node, or from the node's right-click menu. Slash commands (e.g., /character, /artist) always work regardless of this setting.",
|
||||||
category: ["LoRA Manager", "Autocomplete", "Prompt"],
|
category: ["LoRA Manager", "Autocomplete", "Prompt"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -576,6 +596,7 @@ app.registerExtension({
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
PROMPT_TAG_AUTOCOMPLETE_SETTING_ID,
|
||||||
getWheelSensitivity,
|
getWheelSensitivity,
|
||||||
getAutoPathCorrectionPreference,
|
getAutoPathCorrectionPreference,
|
||||||
getAutocompleteAppendCommaPreference,
|
getAutocompleteAppendCommaPreference,
|
||||||
@@ -587,4 +608,5 @@ export {
|
|||||||
getNewTabTemplatePreference,
|
getNewTabTemplatePreference,
|
||||||
getStrengthStepPreference,
|
getStrengthStepPreference,
|
||||||
getLoraActiveFiltersAutocompletePreference,
|
getLoraActiveFiltersAutocompletePreference,
|
||||||
|
setLoraManagerSettingValue,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user