Merge branch 'staging' into proxy-confirm
| @@ -101,6 +101,7 @@ import { | |||
| 101 | proxies, | 101 | proxies, |
| 102 | loadProxyPresets, | 102 | loadProxyPresets, |
| 103 | selected_proxy, | 103 | selected_proxy, |
| 104 | initOpenai, | ||
| 104 | } from './scripts/openai.js'; | 105 | } from './scripts/openai.js'; |
| 105 | 106 | ||
| 106 | import { | 107 | import { |
| @@ -155,7 +156,6 @@ import { | |||
| 155 | ensureImageFormatSupported, | 156 | ensureImageFormatSupported, |
| 156 | flashHighlight, | 157 | flashHighlight, |
| 157 | isTrueBoolean, | 158 | isTrueBoolean, |
| 158 | debouncedThrottle, | ||
| 159 | } from './scripts/utils.js'; | 159 | } from './scripts/utils.js'; |
| 160 | import { debounce_timeout } from './scripts/constants.js'; | 160 | import { debounce_timeout } from './scripts/constants.js'; |
| 161 | 161 | ||
| @@ -915,6 +915,7 @@ async function firstLoadInit() { | |||
| 915 | initKeyboard(); | 915 | initKeyboard(); |
| 916 | initDynamicStyles(); | 916 | initDynamicStyles(); |
| 917 | initTags(); | 917 | initTags(); |
| 918 | initOpenai(); | ||
| 918 | await getUserAvatars(true, user_avatar); | 919 | await getUserAvatars(true, user_avatar); |
| 919 | await getCharacters(); | 920 | await getCharacters(); |
| 920 | await getBackgrounds(); | 921 | await getBackgrounds(); |
| @@ -9233,11 +9234,12 @@ jQuery(async function () { | |||
| 9233 | */ | 9234 | */ |
| 9234 | function autoFitEditTextArea(e) { | 9235 | function autoFitEditTextArea(e) { |
| 9235 | scroll_holder = chatElement[0].scrollTop; | 9236 | scroll_holder = chatElement[0].scrollTop; |
| 9236 | e.style.height = '0'; | 9237 | e.style.height = '0px'; |
| 9237 | e.style.height = `${e.scrollHeight + 4}px`; | 9238 | const newHeight = e.scrollHeight + 4; |
| 9239 | e.style.height = `${newHeight}px`; | ||
| 9238 | is_use_scroll_holder = true; | 9240 | is_use_scroll_holder = true; |
| 9239 | } | 9241 | } |
| 9240 | const autoFitEditTextAreaDebounced = debouncedThrottle(autoFitEditTextArea, debounce_timeout.standard); | 9242 | const autoFitEditTextAreaDebounced = debounce(autoFitEditTextArea, debounce_timeout.short); |
| 9241 | document.addEventListener('input', e => { | 9243 | document.addEventListener('input', e => { |
| 9242 | if (e.target instanceof HTMLTextAreaElement && e.target.classList.contains('edit_textarea')) { | 9244 | if (e.target instanceof HTMLTextAreaElement && e.target.classList.contains('edit_textarea')) { |
| 9243 | const immediately = e.target.scrollHeight > e.target.offsetHeight || e.target.value === ''; | 9245 | const immediately = e.target.scrollHeight > e.target.offsetHeight || e.target.value === ''; |
| @@ -696,18 +696,18 @@ const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1; | |||
| 696 | function autoFitSendTextArea() { | 696 | function autoFitSendTextArea() { |
| 697 | const originalScrollBottom = chatBlock.scrollHeight - (chatBlock.scrollTop + chatBlock.offsetHeight); | 697 | const originalScrollBottom = chatBlock.scrollHeight - (chatBlock.scrollTop + chatBlock.offsetHeight); |
| 698 | if (Math.ceil(sendTextArea.scrollHeight + 3) >= Math.floor(sendTextArea.offsetHeight)) { | 698 | if (Math.ceil(sendTextArea.scrollHeight + 3) >= Math.floor(sendTextArea.offsetHeight)) { |
| 699 | // Needs to be pulled dynamically because it is affected by font size changes | 699 | const sendTextAreaMinHeight = '0px'; |
| 700 | const sendTextAreaMinHeight = window.getComputedStyle(sendTextArea).getPropertyValue('min-height'); | ||
| 701 | sendTextArea.style.height = sendTextAreaMinHeight; | 700 | sendTextArea.style.height = sendTextAreaMinHeight; |
| 702 | } | 701 | } |
| 703 | sendTextArea.style.height = sendTextArea.scrollHeight + 3 + 'px'; | 702 | const newHeight = sendTextArea.scrollHeight + 3; |
| 703 | sendTextArea.style.height = `${newHeight}px`; | ||
| 704 | 704 | ||
| 705 | if (!isFirefox) { | 705 | if (!isFirefox) { |
| 706 | const newScrollTop = Math.round(chatBlock.scrollHeight - (chatBlock.offsetHeight + originalScrollBottom)); | 706 | const newScrollTop = Math.round(chatBlock.scrollHeight - (chatBlock.offsetHeight + originalScrollBottom)); |
| 707 | chatBlock.scrollTop = newScrollTop; | 707 | chatBlock.scrollTop = newScrollTop; |
| 708 | } | 708 | } |
| 709 | } | 709 | } |
| 710 | export const autoFitSendTextAreaDebounced = debounce(autoFitSendTextArea); | 710 | export const autoFitSendTextAreaDebounced = debounce(autoFitSendTextArea, debounce_timeout.short); |
| 711 | 711 | ||
| 712 | // --------------------------------------------------- | 712 | // --------------------------------------------------- |
| 713 | 713 | ||
| @@ -882,7 +882,8 @@ export function initRossMods() { | |||
| 882 | }); | 882 | }); |
| 883 | 883 | ||
| 884 | $(sendTextArea).on('input', () => { | 884 | $(sendTextArea).on('input', () => { |
| 885 | if (sendTextArea.scrollHeight > sendTextArea.offsetHeight || sendTextArea.value === '') { | 885 | const scrollbarShown = sendTextArea.clientWidth < sendTextArea.offsetWidth && sendTextArea.offsetHeight >= window.innerHeight / 2; |
| 886 | if ((sendTextArea.scrollHeight > sendTextArea.offsetHeight && !scrollbarShown) || sendTextArea.value === '') { | ||
| 886 | autoFitSendTextArea(); | 887 | autoFitSendTextArea(); |
| 887 | } else { | 888 | } else { |
| 888 | autoFitSendTextAreaDebounced(); | 889 | autoFitSendTextAreaDebounced(); |
| @@ -333,8 +333,9 @@ async function getCaptionForFile(file, prompt, quiet) { | |||
| 333 | return caption; | 333 | return caption; |
| 334 | } | 334 | } |
| 335 | catch (error) { | 335 | catch (error) { |
| 336 | toastr.error('Failed to caption image.'); | 336 | const errorMessage = error.message || 'Unknown error'; |
| 337 | console.log(error); | 337 | toastr.error(errorMessage, "Failed to caption image."); |
| 338 | console.error(error); | ||
| 338 | return ''; | 339 | return ''; |
| 339 | } | 340 | } |
| 340 | finally { | 341 | finally { |
| @@ -914,7 +914,7 @@ jQuery(async function () { | |||
| 914 | 914 | ||
| 915 | await addExtensionControls(); | 915 | await addExtensionControls(); |
| 916 | loadSettings(); | 916 | loadSettings(); |
| 917 | eventSource.on(event_types.MESSAGE_RECEIVED, onChatEvent); | 917 | eventSource.makeLast(event_types.CHARACTER_MESSAGE_RENDERED, onChatEvent); |
| 918 | eventSource.on(event_types.MESSAGE_DELETED, onChatEvent); | 918 | eventSource.on(event_types.MESSAGE_DELETED, onChatEvent); |
| 919 | eventSource.on(event_types.MESSAGE_EDITED, onChatEvent); | 919 | eventSource.on(event_types.MESSAGE_EDITED, onChatEvent); |
| 920 | eventSource.on(event_types.MESSAGE_SWIPED, onChatEvent); | 920 | eventSource.on(event_types.MESSAGE_SWIPED, onChatEvent); |
| @@ -239,7 +239,7 @@ eventSource.on(event_types.CHAT_CHANGED, (...args)=>executeIfReadyElseQueue(onCh | |||
| 239 | const onUserMessage = async () => { | 239 | const onUserMessage = async () => { |
| 240 | await autoExec.handleUser(); | 240 | await autoExec.handleUser(); |
| 241 | }; | 241 | }; |
| 242 | eventSource.on(event_types.USER_MESSAGE_RENDERED, (...args)=>executeIfReadyElseQueue(onUserMessage, args)); | 242 | eventSource.makeFirst(event_types.USER_MESSAGE_RENDERED, (...args)=>executeIfReadyElseQueue(onUserMessage, args)); |
| 243 | 243 | ||
| 244 | const onAiMessage = async (messageId) => { | 244 | const onAiMessage = async (messageId) => { |
| 245 | if (['...'].includes(chat[messageId]?.mes)) { | 245 | if (['...'].includes(chat[messageId]?.mes)) { |
| @@ -249,7 +249,7 @@ const onAiMessage = async (messageId) => { | |||
| 249 | 249 | ||
| 250 | await autoExec.handleAi(); | 250 | await autoExec.handleAi(); |
| 251 | }; | 251 | }; |
| 252 | eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, (...args)=>executeIfReadyElseQueue(onAiMessage, args)); | 252 | eventSource.makeFirst(event_types.CHARACTER_MESSAGE_RENDERED, (...args)=>executeIfReadyElseQueue(onAiMessage, args)); |
| 253 | 253 | ||
| 254 | const onGroupMemberDraft = async () => { | 254 | const onGroupMemberDraft = async () => { |
| 255 | await autoExec.handleGroupMemberDraft(); | 255 | await autoExec.handleGroupMemberDraft(); |
| @@ -4744,22 +4744,23 @@ function runProxyCallback(_, value) { | |||
| 4744 | return foundName; | 4744 | return foundName; |
| 4745 | } | 4745 | } |
| 4746 | 4746 | ||
| 4747 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | 4747 | export function initOpenai() { |
| 4748 | name: 'proxy', | 4748 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 4749 | callback: runProxyCallback, | 4749 | name: 'proxy', |
| 4750 | returns: 'current proxy', | 4750 | callback: runProxyCallback, |
| 4751 | namedArgumentList: [], | 4751 | returns: 'current proxy', |
| 4752 | unnamedArgumentList: [ | 4752 | namedArgumentList: [], |
| 4753 | SlashCommandArgument.fromProps({ | 4753 | unnamedArgumentList: [ |
| 4754 | description: 'name', | 4754 | SlashCommandArgument.fromProps({ |
| 4755 | typeList: [ARGUMENT_TYPE.STRING], | 4755 | description: 'name', |
| 4756 | isRequired: true, | 4756 | typeList: [ARGUMENT_TYPE.STRING], |
| 4757 | enumProvider: () => proxies.map(preset => new SlashCommandEnumValue(preset.name, preset.url)), | 4757 | isRequired: true, |
| 4758 | }), | 4758 | enumProvider: () => proxies.map(preset => new SlashCommandEnumValue(preset.name, preset.url)), |
| 4759 | ], | 4759 | }), |
| 4760 | helpString: 'Sets a proxy preset by name.', | 4760 | ], |
| 4761 | })); | 4761 | helpString: 'Sets a proxy preset by name.', |
| 4762 | 4762 | })); | |
| 4763 | } | ||
| 4763 | 4764 | ||
| 4764 | $(document).ready(async function () { | 4765 | $(document).ready(async function () { |
| 4765 | $('#test_api_button').on('click', testApiConnection); | 4766 | $('#test_api_button').on('click', testApiConnection); |
| @@ -194,7 +194,7 @@ export class Popup { | |||
| 194 | const buttonElement = document.createElement('div'); | 194 | const buttonElement = document.createElement('div'); |
| 195 | buttonElement.classList.add('menu_button', 'popup-button-custom', 'result-control'); | 195 | buttonElement.classList.add('menu_button', 'popup-button-custom', 'result-control'); |
| 196 | buttonElement.classList.add(...(button.classes ?? [])); | 196 | buttonElement.classList.add(...(button.classes ?? [])); |
| 197 | buttonElement.dataset.result = String(button.result ?? undefined); | 197 | buttonElement.dataset.result = String(button.result); // This is expected to also write 'null' or 'staging', to indicate cancel and no action respectively |
| 198 | buttonElement.textContent = button.text; | 198 | buttonElement.textContent = button.text; |
| 199 | buttonElement.dataset.i18n = buttonElement.textContent; | 199 | buttonElement.dataset.i18n = buttonElement.textContent; |
| 200 | buttonElement.tabIndex = 0; | 200 | buttonElement.tabIndex = 0; |
| @@ -317,9 +317,14 @@ export class Popup { | |||
| 317 | // Bind event listeners for all result controls to their defined event type | 317 | // Bind event listeners for all result controls to their defined event type |
| 318 | this.dlg.querySelectorAll('[data-result]').forEach(resultControl => { | 318 | this.dlg.querySelectorAll('[data-result]').forEach(resultControl => { |
| 319 | if (!(resultControl instanceof HTMLElement)) return; | 319 | if (!(resultControl instanceof HTMLElement)) return; |
| 320 | const result = Number(resultControl.dataset.result); | 320 | // If no value was set, we exit out and don't bind an action |
| 321 | if (String(undefined) === String(resultControl.dataset.result)) return; | 321 | if (String(resultControl.dataset.result) === String(undefined)) return; |
| 322 | if (isNaN(result)) throw new Error('Invalid result control. Result must be a number. ' + resultControl.dataset.result); | 322 | |
| 323 | // Make sure that both `POPUP_RESULT` numbers and also `null` as 'cancelled' are supported | ||
| 324 | const result = String(resultControl.dataset.result) === String(null) ? null | ||
| 325 | : Number(resultControl.dataset.result); | ||
| 326 | |||
| 327 | if (result !== null && isNaN(result)) throw new Error('Invalid result control. Result must be a number. ' + resultControl.dataset.result); | ||
| 323 | const type = resultControl.dataset.resultEvent || 'click'; | 328 | const type = resultControl.dataset.resultEvent || 'click'; |
| 324 | resultControl.addEventListener(type, async () => await this.complete(result)); | 329 | resultControl.addEventListener(type, async () => await this.complete(result)); |
| 325 | }); | 330 | }); |
| @@ -2734,45 +2734,26 @@ async function doDelMode(_, text) { | |||
| 2734 | return ''; | 2734 | return ''; |
| 2735 | } | 2735 | } |
| 2736 | 2736 | ||
| 2737 | //first enter delmode | 2737 | // Just enter the delete mode. |
| 2738 | $('#option_delete_mes').trigger('click', { fromSlashCommand: true }); | 2738 | if (!text) { |
| 2739 | 2739 | $('#option_delete_mes').trigger('click', { fromSlashCommand: true }); | |
| 2740 | //parse valid args | 2740 | return ''; |
| 2741 | if (text) { | 2741 | } |
| 2742 | await delay(300); //same as above, need event signal for 'entered del mode' | ||
| 2743 | console.debug('parsing msgs to del'); | ||
| 2744 | let numMesToDel = Number(text); | ||
| 2745 | let lastMesID = Number($('#chat .mes').last().attr('mesid')); | ||
| 2746 | let oldestMesIDToDel = lastMesID - numMesToDel + 1; | ||
| 2747 | |||
| 2748 | if (oldestMesIDToDel < 0) { | ||
| 2749 | toastr.warning(`Cannot delete more than ${chat.length} messages.`); | ||
| 2750 | return ''; | ||
| 2751 | } | ||
| 2752 | |||
| 2753 | let oldestMesToDel = $('#chat').find(`.mes[mesid=${oldestMesIDToDel}]`); | ||
| 2754 | 2742 | ||
| 2755 | if (!oldestMesIDToDel && lastMesID > 0) { | 2743 | const count = Number(text); |
| 2756 | oldestMesToDel = await loadUntilMesId(oldestMesIDToDel); | ||
| 2757 | 2744 | ||
| 2758 | if (!oldestMesToDel || !oldestMesToDel.length) { | 2745 | // Nothing to delete. |
| 2759 | return ''; | 2746 | if (count < 1) { |
| 2760 | } | 2747 | return ''; |
| 2761 | } | 2748 | } |
| 2762 | |||
| 2763 | let oldestDelMesCheckbox = $(oldestMesToDel).find('.del_checkbox'); | ||
| 2764 | let newLastMesID = oldestMesIDToDel - 1; | ||
| 2765 | console.debug(`DelMesReport -- numMesToDel: ${numMesToDel}, lastMesID: ${lastMesID}, oldestMesIDToDel:${oldestMesIDToDel}, newLastMesID: ${newLastMesID}`); | ||
| 2766 | oldestDelMesCheckbox.trigger('click'); | ||
| 2767 | let trueNumberOfDeletedMessage = lastMesID - oldestMesIDToDel + 1; | ||
| 2768 | 2749 | ||
| 2769 | //await delay(1) | 2750 | if (count > chat.length) { |
| 2770 | $('#dialogue_del_mes_ok').trigger('click'); | 2751 | toastr.warning(`Cannot delete more than ${chat.length} messages.`); |
| 2771 | toastr.success(`Deleted ${trueNumberOfDeletedMessage} messages.`); | ||
| 2772 | return ''; | 2752 | return ''; |
| 2773 | } | 2753 | } |
| 2774 | 2754 | ||
| 2775 | return ''; | 2755 | const range = `${chat.length - count}-${chat.length - 1}`; |
| 2756 | return doMesCut(_, range); | ||
| 2776 | } | 2757 | } |
| 2777 | 2758 | ||
| 2778 | function doResetPanels() { | 2759 | function doResetPanels() { |
| @@ -1,6 +1,5 @@ | |||
| 1 | import { | 1 | import { |
| 2 | amount_gen, | 2 | amount_gen, |
| 3 | callPopup, | ||
| 4 | characters, | 3 | characters, |
| 5 | eventSource, | 4 | eventSource, |
| 6 | event_types, | 5 | event_types, |
| @@ -19,6 +18,7 @@ import { | |||
| 19 | import { groups, selected_group } from './group-chats.js'; | 18 | import { groups, selected_group } from './group-chats.js'; |
| 20 | import { instruct_presets } from './instruct-mode.js'; | 19 | import { instruct_presets } from './instruct-mode.js'; |
| 21 | import { kai_settings } from './kai-settings.js'; | 20 | import { kai_settings } from './kai-settings.js'; |
| 21 | import { Popup } from './popup.js'; | ||
| 22 | import { context_presets, getContextSettings, power_user } from './power-user.js'; | 22 | import { context_presets, getContextSettings, power_user } from './power-user.js'; |
| 23 | import { SlashCommand } from './slash-commands/SlashCommand.js'; | 23 | import { SlashCommand } from './slash-commands/SlashCommand.js'; |
| 24 | import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js'; | 24 | import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js'; |
| @@ -165,11 +165,8 @@ class PresetManager { | |||
| 165 | 165 | ||
| 166 | async savePresetAs() { | 166 | async savePresetAs() { |
| 167 | const inputValue = this.getSelectedPresetName(); | 167 | const inputValue = this.getSelectedPresetName(); |
| 168 | const popupText = ` | 168 | const popupText = !this.isNonGenericApi() ? '<h4>Hint: Use a character/group name to bind preset to a specific chat.</h4>' : ''; |
| 169 | <h3>Preset name:</h3> | 169 | const name = await Popup.show.input('Preset name:', popupText, inputValue); |
| 170 | ${!this.isNonGenericApi() ? '<h4>Hint: Use a character/group name to bind preset to a specific chat.</h4>' : ''}`; | ||
| 171 | const name = await callPopup(popupText, 'input', inputValue); | ||
| 172 | |||
| 173 | if (!name) { | 170 | if (!name) { |
| 174 | console.log('Preset name not provided'); | 171 | console.log('Preset name not provided'); |
| 175 | return; | 172 | return; |
| @@ -372,7 +369,7 @@ class PresetManager { | |||
| 372 | if (Object.keys(preset_names).length) { | 369 | if (Object.keys(preset_names).length) { |
| 373 | const nextPresetName = Object.keys(preset_names)[0]; | 370 | const nextPresetName = Object.keys(preset_names)[0]; |
| 374 | const newValue = preset_names[nextPresetName]; | 371 | const newValue = preset_names[nextPresetName]; |
| 375 | $(this.select).find(`option[value="${newValue}"]`).attr('selected', true); | 372 | $(this.select).find(`option[value="${newValue}"]`).attr('selected', 'true'); |
| 376 | $(this.select).trigger('change'); | 373 | $(this.select).trigger('change'); |
| 377 | } | 374 | } |
| 378 | 375 | ||
| @@ -597,8 +594,7 @@ export async function initPresetManager() { | |||
| 597 | return; | 594 | return; |
| 598 | } | 595 | } |
| 599 | 596 | ||
| 600 | const confirm = await callPopup('Delete the preset? This action is irreversible and your current settings will be overwritten.', 'confirm'); | 597 | const confirm = await Popup.show.confirm('Delete the preset?', 'This action is irreversible and your current settings will be overwritten.'); |
| 601 | |||
| 602 | if (!confirm) { | 598 | if (!confirm) { |
| 603 | return; | 599 | return; |
| 604 | } | 600 | } |
| @@ -641,8 +637,7 @@ export async function initPresetManager() { | |||
| 641 | return; | 637 | return; |
| 642 | } | 638 | } |
| 643 | 639 | ||
| 644 | const confirm = await callPopup('<h3>Are you sure?</h3>Resetting a <b>default preset</b> will restore the default settings.', 'confirm'); | 640 | const confirm = await Popup.show.confirm('Are you sure?', 'Resetting a <b>default preset</b> will restore the default settings.'); |
| 645 | |||
| 646 | if (!confirm) { | 641 | if (!confirm) { |
| 647 | return; | 642 | return; |
| 648 | } | 643 | } |
| @@ -653,8 +648,7 @@ export async function initPresetManager() { | |||
| 653 | presetManager.selectPreset(option); | 648 | presetManager.selectPreset(option); |
| 654 | toastr.success('Default preset restored'); | 649 | toastr.success('Default preset restored'); |
| 655 | } else { | 650 | } else { |
| 656 | const confirm = await callPopup('<h3>Are you sure?</h3>Resetting a <b>custom preset</b> will restore to the last saved state.', 'confirm'); | 651 | const confirm = await Popup.show.confirm('Are you sure?', 'Resetting a <b>custom preset</b> will restore to the last saved state.'); |
| 657 | |||
| 658 | if (!confirm) { | 652 | if (!confirm) { |
| 659 | return; | 653 | return; |
| 660 | } | 654 | } |
| @@ -952,14 +952,36 @@ export function initDefaultSlashCommands() { | |||
| 952 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | 952 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 953 | name: 'fuzzy', | 953 | name: 'fuzzy', |
| 954 | callback: fuzzyCallback, | 954 | callback: fuzzyCallback, |
| 955 | returns: 'first matching item', | 955 | returns: 'matching item', |
| 956 | namedArgumentList: [ | 956 | namedArgumentList: [ |
| 957 | new SlashCommandNamedArgument( | 957 | SlashCommandNamedArgument.fromProps({ |
| 958 | 'list', 'list of items to match against', [ARGUMENT_TYPE.LIST], true, | 958 | name: 'list', |
| 959 | ), | 959 | description: 'list of items to match against', |
| 960 | new SlashCommandNamedArgument( | 960 | acceptsMultiple: false, |
| 961 | 'threshold', 'fuzzy match threshold (0.0 to 1.0)', [ARGUMENT_TYPE.NUMBER], false, false, '0.4', | 961 | isRequired: true, |
| 962 | ), | 962 | typeList: [ARGUMENT_TYPE.LIST, ARGUMENT_TYPE.VARIABLE_NAME], |
| 963 | enumProvider: commonEnumProviders.variables('all'), | ||
| 964 | }), | ||
| 965 | SlashCommandNamedArgument.fromProps({ | ||
| 966 | name: 'threshold', | ||
| 967 | description: 'fuzzy match threshold (0.0 to 1.0)', | ||
| 968 | typeList: [ARGUMENT_TYPE.NUMBER], | ||
| 969 | isRequired: false, | ||
| 970 | defaultValue: '0.4', | ||
| 971 | acceptsMultiple: false, | ||
| 972 | }), | ||
| 973 | SlashCommandNamedArgument.fromProps({ | ||
| 974 | name: 'mode', | ||
| 975 | description: 'fuzzy match mode', | ||
| 976 | typeList: [ARGUMENT_TYPE.STRING], | ||
| 977 | isRequired: false, | ||
| 978 | defaultValue: 'first', | ||
| 979 | acceptsMultiple: false, | ||
| 980 | enumList: [ | ||
| 981 | new SlashCommandEnumValue('first', 'first match below the threshold', enumTypes.enum, enumIcons.default), | ||
| 982 | new SlashCommandEnumValue('best', 'best match below the threshold', enumTypes.enum, enumIcons.default), | ||
| 983 | ], | ||
| 984 | }), | ||
| 963 | ], | 985 | ], |
| 964 | unnamedArgumentList: [ | 986 | unnamedArgumentList: [ |
| 965 | new SlashCommandArgument( | 987 | new SlashCommandArgument( |
| @@ -977,6 +999,13 @@ export function initDefaultSlashCommands() { | |||
| 977 | At 1.0 (max) the match is very loose and will match anything. | 999 | At 1.0 (max) the match is very loose and will match anything. |
| 978 | </div> | 1000 | </div> |
| 979 | <div> | 1001 | <div> |
| 1002 | The optional <code>mode</code> argument allows to control the behavior when multiple items match the text. | ||
| 1003 | <ul> | ||
| 1004 | <li><code>first</code> (default) returns the first match below the threshold.</li> | ||
| 1005 | <li><code>best</code> returns the best match below the threshold.</li> | ||
| 1006 | </ul> | ||
| 1007 | </div> | ||
| 1008 | <div> | ||
| 980 | The returned value passes to the next command through the pipe. | 1009 | The returned value passes to the next command through the pipe. |
| 981 | </div> | 1010 | </div> |
| 982 | <div> | 1011 | <div> |
| @@ -1865,7 +1894,7 @@ async function inputCallback(args, prompt) { | |||
| 1865 | * @param {FuzzyCommandArgs} args - arguments containing "list" (JSON array) and optionaly "threshold" (float between 0.0 and 1.0) | 1894 | * @param {FuzzyCommandArgs} args - arguments containing "list" (JSON array) and optionaly "threshold" (float between 0.0 and 1.0) |
| 1866 | * @param {string} searchInValue - the string where items of list are searched | 1895 | * @param {string} searchInValue - the string where items of list are searched |
| 1867 | * @returns {string} - the matched item from the list | 1896 | * @returns {string} - the matched item from the list |
| 1868 | * @typedef {{list: string, threshold: string}} FuzzyCommandArgs - arguments for /fuzzy command | 1897 | * @typedef {{list: string, threshold: string, mode:string}} FuzzyCommandArgs - arguments for /fuzzy command |
| 1869 | * @example /fuzzy list=["down","left","up","right"] "he looks up" | /echo // should return "up" | 1898 | * @example /fuzzy list=["down","left","up","right"] "he looks up" | /echo // should return "up" |
| 1870 | * @link https://www.fusejs.io/ | 1899 | * @link https://www.fusejs.io/ |
| 1871 | */ | 1900 | */ |
| @@ -1895,7 +1924,7 @@ function fuzzyCallback(args, searchInValue) { | |||
| 1895 | }; | 1924 | }; |
| 1896 | // threshold determines how strict is the match, low threshold value is very strict, at 1 (nearly?) everything matches | 1925 | // threshold determines how strict is the match, low threshold value is very strict, at 1 (nearly?) everything matches |
| 1897 | if ('threshold' in args) { | 1926 | if ('threshold' in args) { |
| 1898 | params.threshold = parseFloat(resolveVariable(args.threshold)); | 1927 | params.threshold = parseFloat(args.threshold); |
| 1899 | if (isNaN(params.threshold)) { | 1928 | if (isNaN(params.threshold)) { |
| 1900 | console.warn('WARN: \'threshold\' argument must be a float between 0.0 and 1.0 for /fuzzy command'); | 1929 | console.warn('WARN: \'threshold\' argument must be a float between 0.0 and 1.0 for /fuzzy command'); |
| 1901 | return ''; | 1930 | return ''; |
| @@ -1908,16 +1937,42 @@ function fuzzyCallback(args, searchInValue) { | |||
| 1908 | } | 1937 | } |
| 1909 | } | 1938 | } |
| 1910 | 1939 | ||
| 1911 | const fuse = new Fuse([searchInValue], params); | 1940 | function getFirstMatch() { |
| 1912 | // each item in the "list" is searched within "search_item", if any matches it returns the matched "item" | 1941 | const fuse = new Fuse([searchInValue], params); |
| 1913 | for (const searchItem of list) { | 1942 | // each item in the "list" is searched within "search_item", if any matches it returns the matched "item" |
| 1914 | const result = fuse.search(searchItem); | 1943 | for (const searchItem of list) { |
| 1944 | const result = fuse.search(searchItem); | ||
| 1945 | console.debug('/fuzzy: result', result); | ||
| 1946 | if (result.length > 0) { | ||
| 1947 | console.info('/fuzzy: first matched', searchItem); | ||
| 1948 | return searchItem; | ||
| 1949 | } | ||
| 1950 | } | ||
| 1951 | |||
| 1952 | console.info('/fuzzy: no match'); | ||
| 1953 | return ''; | ||
| 1954 | } | ||
| 1955 | |||
| 1956 | function getBestMatch() { | ||
| 1957 | const fuse = new Fuse(list, params); | ||
| 1958 | const result = fuse.search(searchInValue); | ||
| 1959 | console.debug('/fuzzy: result', result); | ||
| 1915 | if (result.length > 0) { | 1960 | if (result.length > 0) { |
| 1916 | console.info('fuzzyCallback Matched: ' + searchItem); | 1961 | console.info('/fuzzy: best matched', result[0].item); |
| 1917 | return searchItem; | 1962 | return result[0].item; |
| 1918 | } | 1963 | } |
| 1964 | |||
| 1965 | console.info('/fuzzy: no match'); | ||
| 1966 | return ''; | ||
| 1967 | } | ||
| 1968 | |||
| 1969 | switch (String(args.mode).trim().toLowerCase()) { | ||
| 1970 | case 'best': | ||
| 1971 | return getBestMatch(); | ||
| 1972 | case 'first': | ||
| 1973 | default: | ||
| 1974 | return getFirstMatch(); | ||
| 1919 | } | 1975 | } |
| 1920 | return ''; | ||
| 1921 | } catch { | 1976 | } catch { |
| 1922 | console.warn('WARN: Invalid list argument provided for /fuzzy command'); | 1977 | console.warn('WARN: Invalid list argument provided for /fuzzy command'); |
| 1923 | return ''; | 1978 | return ''; |
| @@ -1,11 +1,11 @@ | |||
| 1 | import { chat_metadata, characters, substituteParams, chat, extension_prompt_roles, extension_prompt_types } from "../../script.js"; | 1 | import { chat_metadata, characters, substituteParams, chat, extension_prompt_roles, extension_prompt_types } from '../../script.js'; |
| 2 | import { extension_settings } from "../extensions.js"; | 2 | import { extension_settings } from '../extensions.js'; |
| 3 | import { getGroupMembers, groups, selected_group } from "../group-chats.js"; | 3 | import { getGroupMembers, groups } from '../group-chats.js'; |
| 4 | import { power_user } from "../power-user.js"; | 4 | import { power_user } from '../power-user.js'; |
| 5 | import { searchCharByName, getTagsList, tags } from "../tags.js"; | 5 | import { searchCharByName, getTagsList, tags } from '../tags.js'; |
| 6 | import { SlashCommandClosure } from "./SlashCommandClosure.js"; | 6 | import { world_names } from '../world-info.js'; |
| 7 | import { SlashCommandEnumValue, enumTypes } from "./SlashCommandEnumValue.js"; | 7 | import { SlashCommandClosure } from './SlashCommandClosure.js'; |
| 8 | import { SlashCommandExecutor } from "./SlashCommandExecutor.js"; | 8 | import { SlashCommandEnumValue, enumTypes } from './SlashCommandEnumValue.js'; |
| 9 | 9 | ||
| 10 | /** | 10 | /** |
| 11 | * A collection of regularly used enum icons | 11 | * A collection of regularly used enum icons |
| @@ -103,8 +103,8 @@ export const enumIcons = { | |||
| 103 | // Remove possible nullable types definition to match type icon | 103 | // Remove possible nullable types definition to match type icon |
| 104 | type = type.replace(/\?$/, ''); | 104 | type = type.replace(/\?$/, ''); |
| 105 | return enumIcons[type] ?? enumIcons.default; | 105 | return enumIcons[type] ?? enumIcons.default; |
| 106 | } | 106 | }, |
| 107 | } | 107 | }; |
| 108 | 108 | ||
| 109 | /** | 109 | /** |
| 110 | * A collection of common enum providers | 110 | * A collection of common enum providers |
| @@ -143,7 +143,7 @@ export const commonEnumProviders = { | |||
| 143 | ...isAll || types.includes('global') ? Object.keys(extension_settings.variables.global ?? []).map(name => new SlashCommandEnumValue(name, null, enumTypes.macro, enumIcons.globalVariable)) : [], | 143 | ...isAll || types.includes('global') ? Object.keys(extension_settings.variables.global ?? []).map(name => new SlashCommandEnumValue(name, null, enumTypes.macro, enumIcons.globalVariable)) : [], |
| 144 | ...isAll || types.includes('local') ? Object.keys(chat_metadata.variables ?? []).map(name => new SlashCommandEnumValue(name, null, enumTypes.name, enumIcons.localVariable)) : [], | 144 | ...isAll || types.includes('local') ? Object.keys(chat_metadata.variables ?? []).map(name => new SlashCommandEnumValue(name, null, enumTypes.name, enumIcons.localVariable)) : [], |
| 145 | ...isAll || types.includes('scope') ? [].map(name => new SlashCommandEnumValue(name, null, enumTypes.variable, enumIcons.scopeVariable)) : [], // TODO: Add scoped variables here, Lenny | 145 | ...isAll || types.includes('scope') ? [].map(name => new SlashCommandEnumValue(name, null, enumTypes.variable, enumIcons.scopeVariable)) : [], // TODO: Add scoped variables here, Lenny |
| 146 | ] | 146 | ]; |
| 147 | }, | 147 | }, |
| 148 | 148 | ||
| 149 | /** | 149 | /** |
| @@ -180,7 +180,7 @@ export const commonEnumProviders = { | |||
| 180 | * @param {('all' | 'existing' | 'not-existing')?} [mode='all'] - Which types of tags to show | 180 | * @param {('all' | 'existing' | 'not-existing')?} [mode='all'] - Which types of tags to show |
| 181 | * @returns {() => SlashCommandEnumValue[]} | 181 | * @returns {() => SlashCommandEnumValue[]} |
| 182 | */ | 182 | */ |
| 183 | tagsForChar: (mode = 'all') => (/** @type {SlashCommandExecutor} */ executor) => { | 183 | tagsForChar: (mode = 'all') => (/** @type {import('./SlashCommandExecutor.js').SlashCommandExecutor} */ executor) => { |
| 184 | // Try to see if we can find the char during execution to filter down the tags list some more. Otherwise take all tags. | 184 | // Try to see if we can find the char during execution to filter down the tags list some more. Otherwise take all tags. |
| 185 | const charName = executor.namedArgumentList.find(it => it.name == 'name')?.value; | 185 | const charName = executor.namedArgumentList.find(it => it.name == 'name')?.value; |
| 186 | if (charName instanceof SlashCommandClosure) throw new Error('Argument \'name\' does not support closures'); | 186 | if (charName instanceof SlashCommandClosure) throw new Error('Argument \'name\' does not support closures'); |
| @@ -213,7 +213,7 @@ export const commonEnumProviders = { | |||
| 213 | * | 213 | * |
| 214 | * @returns {SlashCommandEnumValue[]} | 214 | * @returns {SlashCommandEnumValue[]} |
| 215 | */ | 215 | */ |
| 216 | worlds: () => $('#world_info').children().toArray().map(x => new SlashCommandEnumValue(x.textContent, null, enumTypes.name, enumIcons.world)), | 216 | worlds: () => world_names.map(worldName => new SlashCommandEnumValue(worldName, null, enumTypes.name, enumIcons.world)), |
| 217 | 217 | ||
| 218 | /** | 218 | /** |
| 219 | * All existing injects for the current chat | 219 | * All existing injects for the current chat |
| @@ -14,7 +14,6 @@ import { SlashCommand } from './slash-commands/SlashCommand.js'; | |||
| 14 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; | 14 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js'; |
| 15 | import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js'; | 15 | import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js'; |
| 16 | import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; | 16 | import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 17 | import { SlashCommandExecutor } from './slash-commands/SlashCommandExecutor.js'; | ||
| 18 | import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js'; | 17 | import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js'; |
| 19 | import { callGenericPopup, Popup, POPUP_TYPE } from './popup.js'; | 18 | import { callGenericPopup, Popup, POPUP_TYPE } from './popup.js'; |
| 20 | 19 | ||
| @@ -1215,7 +1214,7 @@ function registerWorldInfoSlashCommands() { | |||
| 1215 | enumTypes.enum, enumIcons.getDataTypeIcon(value.type))), | 1214 | enumTypes.enum, enumIcons.getDataTypeIcon(value.type))), |
| 1216 | 1215 | ||
| 1217 | /** All existing UIDs based on the file argument as world name */ | 1216 | /** All existing UIDs based on the file argument as world name */ |
| 1218 | wiUids: (/** @type {SlashCommandExecutor} */ executor) => { | 1217 | wiUids: (/** @type {import('./slash-commands/SlashCommandExecutor.js').SlashCommandExecutor} */ executor) => { |
| 1219 | const file = executor.namedArgumentList.find(it => it.name == 'file')?.value; | 1218 | const file = executor.namedArgumentList.find(it => it.name == 'file')?.value; |
| 1220 | if (file instanceof SlashCommandClosure) throw new Error('Argument \'file\' does not support closures'); | 1219 | if (file instanceof SlashCommandClosure) throw new Error('Argument \'file\' does not support closures'); |
| 1221 | // Try find world from cache | 1220 | // Try find world from cache |
| @@ -3161,7 +3160,8 @@ function duplicateWorldInfoEntry(data, uid) { | |||
| 3161 | } | 3160 | } |
| 3162 | 3161 | ||
| 3163 | // Exclude uid and gather the rest of the properties | 3162 | // Exclude uid and gather the rest of the properties |
| 3164 | const { uid: _, ...originalData } = data.entries[uid]; | 3163 | const originalData = Object.assign({}, data.entries[uid]); |
| 3164 | delete originalData.uid; | ||
| 3165 | 3165 | ||
| 3166 | // Create new entry and copy over data | 3166 | // Create new entry and copy over data |
| 3167 | const entry = createWorldInfoEntry(data.name, data); | 3167 | const entry = createWorldInfoEntry(data.name, data); |
| @@ -4326,8 +4326,9 @@ function onWorldInfoChange(args, text) { | |||
| 4326 | $('#world_info').val(null).trigger('change'); | 4326 | $('#world_info').val(null).trigger('change'); |
| 4327 | } | 4327 | } |
| 4328 | } else { //if it's a pointer selection | 4328 | } else { //if it's a pointer selection |
| 4329 | let tempWorldInfo = []; | 4329 | const tempWorldInfo = []; |
| 4330 | let selectedWorlds = $('#world_info').val().map((e) => Number(e)).filter((e) => !isNaN(e)); | 4330 | const val = $('#world_info').val(); |
| 4331 | const selectedWorlds = (Array.isArray(val) ? val : [val]).map((e) => Number(e)).filter((e) => !isNaN(e)); | ||
| 4331 | if (selectedWorlds.length > 0) { | 4332 | if (selectedWorlds.length > 0) { |
| 4332 | selectedWorlds.forEach((worldIndex) => { | 4333 | selectedWorlds.forEach((worldIndex) => { |
| 4333 | const existingWorldName = world_names[worldIndex]; | 4334 | const existingWorldName = world_names[worldIndex]; |