From e19bf1afdda7a77bc8eb5394334c9ea9bc173973 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Wed, 20 Dec 2023 13:39:09 +0000 Subject: [PATCH 001/522] clean out QR extension --- .../quick-reply/contextMenuEditor.html | 49 - .../scripts/extensions/quick-reply/index.js | 1149 ----------------- .../extensions/quick-reply/manifest.json | 4 +- .../extensions/quick-reply/src/ContextMenu.js | 67 - .../extensions/quick-reply/src/MenuHeader.js | 20 - .../extensions/quick-reply/src/MenuItem.js | 76 -- .../extensions/quick-reply/src/SubMenu.js | 66 - .../scripts/extensions/quick-reply/style.css | 114 -- 8 files changed, 2 insertions(+), 1543 deletions(-) delete mode 100644 public/scripts/extensions/quick-reply/contextMenuEditor.html delete mode 100644 public/scripts/extensions/quick-reply/index.js delete mode 100644 public/scripts/extensions/quick-reply/src/ContextMenu.js delete mode 100644 public/scripts/extensions/quick-reply/src/MenuHeader.js delete mode 100644 public/scripts/extensions/quick-reply/src/MenuItem.js delete mode 100644 public/scripts/extensions/quick-reply/src/SubMenu.js delete mode 100644 public/scripts/extensions/quick-reply/style.css diff --git a/public/scripts/extensions/quick-reply/contextMenuEditor.html b/public/scripts/extensions/quick-reply/contextMenuEditor.html deleted file mode 100644 index 4fa470bdc..000000000 --- a/public/scripts/extensions/quick-reply/contextMenuEditor.html +++ /dev/null @@ -1,49 +0,0 @@ -
-
-

Context Menu Editor

-
- -
-
- -
-

Auto-Execute

-
- - - - - -
-

UI Options

-
- - -
-
-
diff --git a/public/scripts/extensions/quick-reply/index.js b/public/scripts/extensions/quick-reply/index.js deleted file mode 100644 index 6b184f192..000000000 --- a/public/scripts/extensions/quick-reply/index.js +++ /dev/null @@ -1,1149 +0,0 @@ -import { saveSettingsDebounced, callPopup, getRequestHeaders, substituteParams, eventSource, event_types, animation_duration } from '../../../script.js'; -import { getContext, extension_settings } from '../../extensions.js'; -import { getSortableDelay, escapeHtml, delay } from '../../utils.js'; -import { executeSlashCommands, registerSlashCommand } from '../../slash-commands.js'; -import { ContextMenu } from './src/ContextMenu.js'; -import { MenuItem } from './src/MenuItem.js'; -import { MenuHeader } from './src/MenuHeader.js'; -import { loadMovingUIState } from '../../power-user.js'; -import { dragElement } from '../../RossAscends-mods.js'; - -export { MODULE_NAME }; - -const MODULE_NAME = 'quick-reply'; -const UPDATE_INTERVAL = 1000; -let presets = []; -let selected_preset = ''; - -const defaultSettings = { - quickReplyEnabled: false, - numberOfSlots: 5, - quickReplySlots: [], - placeBeforeInputEnabled: false, - quickActionEnabled: false, - AutoInputInject: true, -}; - -//method from worldinfo -async function updateQuickReplyPresetList() { - const result = await fetch('/api/settings/get', { - method: 'POST', - headers: getRequestHeaders(), - body: JSON.stringify({}), - }); - - if (result.ok) { - var data = await result.json(); - presets = data.quickReplyPresets?.length ? data.quickReplyPresets : []; - console.debug('Quick Reply presets', presets); - $('#quickReplyPresets').find('option[value!=""]').remove(); - - - if (presets !== undefined) { - presets.forEach((item) => { - const option = document.createElement('option'); - option.value = item.name; - option.innerText = item.name; - option.selected = selected_preset.includes(item.name); - $('#quickReplyPresets').append(option); - }); - } - } -} - -async function loadSettings(type) { - if (type === 'init') { - await updateQuickReplyPresetList(); - } - if (Object.keys(extension_settings.quickReply).length === 0) { - Object.assign(extension_settings.quickReply, defaultSettings); - } - - if (extension_settings.quickReply.AutoInputInject === undefined) { - extension_settings.quickReply.AutoInputInject = true; - } - - // If the user has an old version of the extension, update it - if (!Array.isArray(extension_settings.quickReply.quickReplySlots)) { - extension_settings.quickReply.quickReplySlots = []; - extension_settings.quickReply.numberOfSlots = defaultSettings.numberOfSlots; - - for (let i = 1; i <= extension_settings.quickReply.numberOfSlots; i++) { - extension_settings.quickReply.quickReplySlots.push({ - mes: extension_settings.quickReply[`quickReply${i}Mes`], - label: extension_settings.quickReply[`quickReply${i}Label`], - enabled: true, - }); - - delete extension_settings.quickReply[`quickReply${i}Mes`]; - delete extension_settings.quickReply[`quickReply${i}Label`]; - } - } - - initializeEmptySlots(extension_settings.quickReply.numberOfSlots); - generateQuickReplyElements(); - - for (let i = 1; i <= extension_settings.quickReply.numberOfSlots; i++) { - $(`#quickReply${i}Mes`).val(extension_settings.quickReply.quickReplySlots[i - 1]?.mes).trigger('input'); - $(`#quickReply${i}Label`).val(extension_settings.quickReply.quickReplySlots[i - 1]?.label).trigger('input'); - } - - $('#quickReplyEnabled').prop('checked', extension_settings.quickReply.quickReplyEnabled); - $('#quickReplyNumberOfSlots').val(extension_settings.quickReply.numberOfSlots); - $('#placeBeforeInputEnabled').prop('checked', extension_settings.quickReply.placeBeforeInputEnabled); - $('#quickActionEnabled').prop('checked', extension_settings.quickReply.quickActionEnabled); - $('#AutoInputInject').prop('checked', extension_settings.quickReply.AutoInputInject); -} - -function onQuickReplyInput(id) { - extension_settings.quickReply.quickReplySlots[id - 1].mes = $(`#quickReply${id}Mes`).val(); - $(`#quickReply${id}`).attr('title', String($(`#quickReply${id}Mes`).val())); - saveSettingsDebounced(); -} - -function onQuickReplyLabelInput(id) { - extension_settings.quickReply.quickReplySlots[id - 1].label = $(`#quickReply${id}Label`).val(); - addQuickReplyBar(); - saveSettingsDebounced(); -} - -async function onQuickReplyContextMenuChange(id) { - extension_settings.quickReply.quickReplySlots[id - 1].contextMenu = JSON.parse($(`#quickReplyContainer > [data-order="${id}"]`).attr('data-contextMenu')); - saveSettingsDebounced(); -} - -async function onQuickReplyCtxButtonClick(id) { - const editorHtml = $(await $.get('scripts/extensions/quick-reply/contextMenuEditor.html')); - const popupResult = callPopup(editorHtml, 'confirm', undefined, { okButton: 'Save', wide: false, large: false, rows: 1 }); - const qr = extension_settings.quickReply.quickReplySlots[id - 1]; - if (!qr.contextMenu) { - qr.contextMenu = []; - } - /**@type {HTMLTemplateElement}*/ - const tpl = document.querySelector('#quickReply_contextMenuEditor_itemTemplate'); - const fillPresetSelect = (select, item) => { - [{ name: 'Select a preset', value: '' }, ...presets].forEach(preset => { - const opt = document.createElement('option'); { - opt.value = preset.value ?? preset.name; - opt.textContent = preset.name; - opt.selected = preset.name == item.preset; - select.append(opt); - } - }); - }; - const addCtxItem = (item, idx) => { - const dom = tpl.content.cloneNode(true); - const ctxItem = dom.querySelector('.quickReplyContextMenuEditor_item'); - ctxItem.setAttribute('data-order', idx); - const select = ctxItem.querySelector('.quickReply_contextMenuEditor_preset'); - fillPresetSelect(select, item); - dom.querySelector('.quickReply_contextMenuEditor_chaining').checked = item.chain; - $('.quickReply_contextMenuEditor_remove', ctxItem).on('click', () => ctxItem.remove()); - document.querySelector('#quickReply_contextMenuEditor_content').append(ctxItem); - }; - [...qr.contextMenu, {}].forEach((item, idx) => { - addCtxItem(item, idx); - }); - $('#quickReply_contextMenuEditor_addPreset').on('click', () => { - addCtxItem({}, document.querySelector('#quickReply_contextMenuEditor_content').children.length); - }); - - $('#quickReply_contextMenuEditor_content').sortable({ - delay: getSortableDelay(), - stop: () => { }, - }); - - $('#quickReply_autoExecute_userMessage').prop('checked', qr.autoExecute_userMessage ?? false); - $('#quickReply_autoExecute_botMessage').prop('checked', qr.autoExecute_botMessage ?? false); - $('#quickReply_autoExecute_chatLoad').prop('checked', qr.autoExecute_chatLoad ?? false); - $('#quickReply_autoExecute_appStartup').prop('checked', qr.autoExecute_appStartup ?? false); - $('#quickReply_hidden').prop('checked', qr.hidden ?? false); - - $('#quickReply_hidden').on('input', () => { - const state = !!$('#quickReply_hidden').prop('checked'); - qr.hidden = state; - saveSettingsDebounced(); - }); - - $('#quickReply_autoExecute_appStartup').on('input', () => { - const state = !!$('#quickReply_autoExecute_appStartup').prop('checked'); - qr.autoExecute_appStartup = state; - saveSettingsDebounced(); - }); - - $('#quickReply_autoExecute_userMessage').on('input', () => { - const state = !!$('#quickReply_autoExecute_userMessage').prop('checked'); - qr.autoExecute_userMessage = state; - saveSettingsDebounced(); - }); - - $('#quickReply_autoExecute_botMessage').on('input', () => { - const state = !!$('#quickReply_autoExecute_botMessage').prop('checked'); - qr.autoExecute_botMessage = state; - saveSettingsDebounced(); - }); - - $('#quickReply_autoExecute_chatLoad').on('input', () => { - const state = !!$('#quickReply_autoExecute_chatLoad').prop('checked'); - qr.autoExecute_chatLoad = state; - saveSettingsDebounced(); - }); - - $('#quickReply_ui_title').val(qr.title ?? ''); - - if (await popupResult) { - qr.contextMenu = Array.from(document.querySelectorAll('#quickReply_contextMenuEditor_content > .quickReplyContextMenuEditor_item')) - .map(item => ({ - preset: item.querySelector('.quickReply_contextMenuEditor_preset').value, - chain: item.querySelector('.quickReply_contextMenuEditor_chaining').checked, - })) - .filter(item => item.preset); - $(`#quickReplyContainer[data-order="${id}"]`).attr('data-contextMenu', JSON.stringify(qr.contextMenu)); - qr.title = $('#quickReply_ui_title').val(); - saveSettingsDebounced(); - updateQuickReplyPreset(); - onQuickReplyLabelInput(id); - } -} - -async function onQuickReplyEnabledInput() { - let isEnabled = $(this).prop('checked'); - extension_settings.quickReply.quickReplyEnabled = !!isEnabled; - if (isEnabled === true) { - $('#quickReplyBar').show(); - } else { $('#quickReplyBar').hide(); } - saveSettingsDebounced(); -} - -// New function to handle input on quickActionEnabled -async function onQuickActionEnabledInput() { - extension_settings.quickReply.quickActionEnabled = !!$(this).prop('checked'); - saveSettingsDebounced(); -} - -async function onPlaceBeforeInputEnabledInput() { - extension_settings.quickReply.placeBeforeInputEnabled = !!$(this).prop('checked'); - saveSettingsDebounced(); -} - -async function onAutoInputInject() { - extension_settings.quickReply.AutoInputInject = !!$(this).prop('checked'); - saveSettingsDebounced(); -} - -async function sendQuickReply(index) { - const prompt = extension_settings.quickReply.quickReplySlots[index]?.mes || ''; - return await performQuickReply(prompt, index); -} - -async function executeQuickReplyByName(name) { - if (!extension_settings.quickReply.quickReplyEnabled) { - throw new Error('Quick Reply is disabled'); - } - - let qr = extension_settings.quickReply.quickReplySlots.find(x => x.label == name); - - if (!qr && name.includes('.')) { - const [presetName, qrName] = name.split('.'); - const preset = presets.find(x => x.name == presetName); - if (preset) { - qr = preset.quickReplySlots.find(x => x.label == qrName); - } - } - - if (!qr) { - throw new Error(`Quick Reply "${name}" not found`); - } - - return await performQuickReply(qr.mes); -} - -window['executeQuickReplyByName'] = executeQuickReplyByName; - -async function performQuickReply(prompt, index) { - if (!prompt) { - console.warn(`Quick reply slot ${index} is empty! Aborting.`); - return; - } - const existingText = $('#send_textarea').val(); - - let newText; - - if (existingText && extension_settings.quickReply.AutoInputInject) { - if (extension_settings.quickReply.placeBeforeInputEnabled) { - newText = `${prompt} ${existingText} `; - } else { - newText = `${existingText} ${prompt} `; - } - } else { - // If no existing text and placeBeforeInputEnabled false, add prompt only (with a trailing space) - newText = `${prompt} `; - } - - // the prompt starts with '/' - execute slash commands natively - if (prompt.startsWith('/')) { - const result = await executeSlashCommands(newText); - return typeof result === 'object' ? result?.pipe : ''; - } - - newText = substituteParams(newText); - - $('#send_textarea').val(newText); - - // Set the focus back to the textarea - $('#send_textarea').trigger('focus'); - - // Only trigger send button if quickActionEnabled is not checked or - if (!extension_settings.quickReply.quickActionEnabled) { - $('#send_but').trigger('click'); - } -} - - -function buildContextMenu(qr, chainMes = null, hierarchy = [], labelHierarchy = []) { - const tree = { - label: qr.label, - mes: (chainMes && qr.mes ? `${chainMes} | ` : '') + qr.mes, - children: [], - }; - qr.contextMenu?.forEach(ctxItem => { - let chain = ctxItem.chain; - let subName = ctxItem.preset; - const sub = presets.find(it => it.name == subName); - if (sub) { - // prevent circular references - if (hierarchy.indexOf(sub.name) == -1) { - const nextHierarchy = [...hierarchy, sub.name]; - const nextLabelHierarchy = [...labelHierarchy, tree.label]; - tree.children.push(new MenuHeader(sub.name)); - sub.quickReplySlots.forEach(subQr => { - const subInfo = buildContextMenu(subQr, chain ? tree.mes : null, nextHierarchy, nextLabelHierarchy); - tree.children.push(new MenuItem( - subInfo.label, - subInfo.mes, - (evt) => { - evt.stopPropagation(); - performQuickReply(subInfo.mes.replace(/%%parent(-\d+)?%%/g, (_, index) => { - return nextLabelHierarchy.slice(parseInt(index ?? '-1'))[0]; - })); - }, - subInfo.children, - )); - }); - } - } - }); - return tree; -} - -async function doQuickReplyBarPopout() { - //shared elements - const newQuickRepliesDiv = '
'; - const popoutButtonClone = $('#quickReplyPopoutButton'); - - if ($('#quickReplyBarPopout').length === 0) { - console.debug('did not see popout yet, creating'); - const template = $('#zoomed_avatar_template').html(); - const controlBarHtml = `
-
-
-
`; - const newElement = $(template); - let quickRepliesClone = $('#quickReplies').html(); - newElement.attr('id', 'quickReplyBarPopout') - .removeClass('zoomed_avatar') - .addClass('draggable scrollY') - .empty() - .append(controlBarHtml) - .append(newQuickRepliesDiv); - //empty original bar - $('#quickReplyBar').empty(); - //add clone in popout - $('body').append(newElement); - $('#quickReplies').append(quickRepliesClone).css('margin-top', '1em'); - $('.quickReplyButton').on('click', function () { - let index = $(this).data('index'); - sendQuickReply(index); - }); - $('.quickReplyButton > .ctx-expander').on('click', function (evt) { - evt.stopPropagation(); - let index = $(this.closest('.quickReplyButton')).data('index'); - const qr = extension_settings.quickReply.quickReplySlots[index]; - if (qr.contextMenu?.length) { - evt.preventDefault(); - const tree = buildContextMenu(qr); - const menu = new ContextMenu(tree.children); - menu.show(evt); - } - }); - $('.quickReplyButton').on('contextmenu', function (evt) { - let index = $(this).data('index'); - const qr = extension_settings.quickReply.quickReplySlots[index]; - if (qr.contextMenu?.length) { - evt.preventDefault(); - const tree = buildContextMenu(qr); - const menu = new ContextMenu(tree.children); - menu.show(evt); - } - }); - - loadMovingUIState(); - $('#quickReplyBarPopout').fadeIn(animation_duration); - dragElement(newElement); - - $('#quickReplyBarPopoutClose').off('click').on('click', function () { - console.debug('saw existing popout, removing'); - let quickRepliesClone = $('#quickReplies').html(); - $('#quickReplyBar').append(newQuickRepliesDiv); - $('#quickReplies').prepend(quickRepliesClone); - $('#quickReplyBar').append(popoutButtonClone).fadeIn(animation_duration); - $('#quickReplyBarPopout').fadeOut(animation_duration, () => { $('#quickReplyBarPopout').remove(); }); - $('.quickReplyButton').on('click', function () { - let index = $(this).data('index'); - sendQuickReply(index); - }); - $('.quickReplyButton > .ctx-expander').on('click', function (evt) { - evt.stopPropagation(); - let index = $(this.closest('.quickReplyButton')).data('index'); - const qr = extension_settings.quickReply.quickReplySlots[index]; - if (qr.contextMenu?.length) { - evt.preventDefault(); - const tree = buildContextMenu(qr); - const menu = new ContextMenu(tree.children); - menu.show(evt); - } - }); - $('.quickReplyButton').on('contextmenu', function (evt) { - let index = $(this).data('index'); - const qr = extension_settings.quickReply.quickReplySlots[index]; - if (qr.contextMenu?.length) { - evt.preventDefault(); - const tree = buildContextMenu(qr); - const menu = new ContextMenu(tree.children); - menu.show(evt); - } - }); - $('#quickReplyPopoutButton').off('click').on('click', doQuickReplyBarPopout); - }); - - } -} - -function addQuickReplyBar() { - let quickReplyButtonHtml = ''; - var targetContainer; - if ($('#quickReplyBarPopout').length !== 0) { - targetContainer = 'popout'; - } else { - targetContainer = 'bar'; - $('#quickReplyBar').remove(); - } - - for (let i = 0; i < extension_settings.quickReply.numberOfSlots; i++) { - const qr = extension_settings.quickReply.quickReplySlots[i]; - const quickReplyMes = qr?.mes || ''; - const quickReplyLabel = qr?.label || ''; - const hidden = qr?.hidden ?? false; - let expander = ''; - if (extension_settings.quickReply.quickReplySlots[i]?.contextMenu?.length) { - expander = ''; - } - quickReplyButtonHtml += `
${DOMPurify.sanitize(quickReplyLabel)}${expander}
`; - } - - const quickReplyBarFullHtml = ` -
-
- ${quickReplyButtonHtml} -
- -
- `; - - if (targetContainer === 'bar') { - $('#send_form').prepend(quickReplyBarFullHtml); - } else { - $('#quickReplies').empty().append(quickReplyButtonHtml); - } - - - $('.quickReplyButton').on('click', function () { - let index = $(this).data('index'); - sendQuickReply(index); - }); - $('#quickReplyPopoutButton').off('click').on('click', doQuickReplyBarPopout); - $('.quickReplyButton > .ctx-expander').on('click', function (evt) { - evt.stopPropagation(); - let index = $(this.closest('.quickReplyButton')).data('index'); - const qr = extension_settings.quickReply.quickReplySlots[index]; - if (qr.contextMenu?.length) { - evt.preventDefault(); - const tree = buildContextMenu(qr); - const menu = new ContextMenu(tree.children); - menu.show(evt); - } - }); - $('.quickReplyButton').on('contextmenu', function (evt) { - let index = $(this).data('index'); - const qr = extension_settings.quickReply.quickReplySlots[index]; - if (qr.contextMenu?.length) { - evt.preventDefault(); - const tree = buildContextMenu(qr); - const menu = new ContextMenu(tree.children); - menu.show(evt); - } - }); -} - -async function moduleWorker() { - if (extension_settings.quickReply.quickReplyEnabled === true) { - $('#quickReplyBar').toggle(getContext().onlineStatus !== 'no_connection'); - } - if (extension_settings.quickReply.selectedPreset) { - selected_preset = extension_settings.quickReply.selectedPreset; - } -} - -async function saveQuickReplyPreset() { - const name = await callPopup('Enter a name for the Quick Reply Preset:', 'input'); - - if (!name) { - return; - } - - const quickReplyPreset = { - name: name, - quickReplyEnabled: extension_settings.quickReply.quickReplyEnabled, - quickReplySlots: extension_settings.quickReply.quickReplySlots, - numberOfSlots: extension_settings.quickReply.numberOfSlots, - AutoInputInject: extension_settings.quickReply.AutoInputInject, - selectedPreset: name, - }; - - const response = await fetch('/savequickreply', { - method: 'POST', - headers: getRequestHeaders(), - body: JSON.stringify(quickReplyPreset), - }); - - if (response.ok) { - const quickReplyPresetIndex = presets.findIndex(x => x.name == name); - - if (quickReplyPresetIndex == -1) { - presets.push(quickReplyPreset); - const option = document.createElement('option'); - option.selected = true; - option.value = name; - option.innerText = name; - $('#quickReplyPresets').append(option); - } - else { - presets[quickReplyPresetIndex] = quickReplyPreset; - $(`#quickReplyPresets option[value="${name}"]`).prop('selected', true); - } - saveSettingsDebounced(); - } else { - toastr.warning('Failed to save Quick Reply Preset.'); - } -} - -//just a copy of save function with the name hardcoded to currently selected preset -async function updateQuickReplyPreset() { - const name = $('#quickReplyPresets').val(); - - if (!name) { - return; - } - - const quickReplyPreset = { - name: name, - quickReplyEnabled: extension_settings.quickReply.quickReplyEnabled, - quickReplySlots: extension_settings.quickReply.quickReplySlots, - numberOfSlots: extension_settings.quickReply.numberOfSlots, - AutoInputInject: extension_settings.quickReply.AutoInputInject, - selectedPreset: name, - }; - - const response = await fetch('/savequickreply', { - method: 'POST', - headers: getRequestHeaders(), - body: JSON.stringify(quickReplyPreset), - }); - - if (response.ok) { - const quickReplyPresetIndex = presets.findIndex(x => x.name == name); - - if (quickReplyPresetIndex == -1) { - presets.push(quickReplyPreset); - const option = document.createElement('option'); - option.selected = true; - option.value = name; - option.innerText = name; - $('#quickReplyPresets').append(option); - } - else { - presets[quickReplyPresetIndex] = quickReplyPreset; - $(`#quickReplyPresets option[value="${name}"]`).prop('selected', true); - } - saveSettingsDebounced(); - } else { - toastr.warning('Failed to save Quick Reply Preset.'); - } -} - -async function onQuickReplyNumberOfSlotsInput() { - const $input = $('#quickReplyNumberOfSlots'); - let numberOfSlots = Number($input.val()); - - if (isNaN(numberOfSlots)) { - numberOfSlots = defaultSettings.numberOfSlots; - } - - // Clamp min and max values (from input attributes) - if (numberOfSlots < Number($input.attr('min'))) { - numberOfSlots = Number($input.attr('min')); - } else if (numberOfSlots > Number($input.attr('max'))) { - numberOfSlots = Number($input.attr('max')); - } - - extension_settings.quickReply.numberOfSlots = numberOfSlots; - extension_settings.quickReply.quickReplySlots.length = numberOfSlots; - - // Initialize new slots - initializeEmptySlots(numberOfSlots); - - await loadSettings(); - addQuickReplyBar(); - moduleWorker(); - saveSettingsDebounced(); -} - -function initializeEmptySlots(numberOfSlots) { - for (let i = 0; i < numberOfSlots; i++) { - if (!extension_settings.quickReply.quickReplySlots[i]) { - extension_settings.quickReply.quickReplySlots[i] = { - mes: '', - label: '', - enabled: true, - }; - } - } -} - -function generateQuickReplyElements() { - let quickReplyHtml = ''; - - for (let i = 1; i <= extension_settings.quickReply.numberOfSlots; i++) { - quickReplyHtml += ` -
- - - - - -
- `; - } - - $('#quickReplyContainer').empty().append(quickReplyHtml); - - for (let i = 1; i <= extension_settings.quickReply.numberOfSlots; i++) { - $(`#quickReply${i}Mes`).on('input', function () { onQuickReplyInput(this.closest('[data-order]').getAttribute('data-order')); }); - $(`#quickReply${i}Label`).on('input', function () { onQuickReplyLabelInput(this.closest('[data-order]').getAttribute('data-order')); }); - $(`#quickReply${i}CtxButton`).on('click', function () { onQuickReplyCtxButtonClick(this.closest('[data-order]').getAttribute('data-order')); }); - $(`#quickReplyContainer > [data-order="${i}"]`).attr('data-contextMenu', JSON.stringify(extension_settings.quickReply.quickReplySlots[i - 1]?.contextMenu ?? [])); - } -} - -async function applyQuickReplyPreset(name) { - const quickReplyPreset = presets.find(x => x.name == name); - - if (!quickReplyPreset) { - toastr.warning(`error, QR preset '${name}' not found. Confirm you are using proper case sensitivity!`); - return; - } - - extension_settings.quickReply = quickReplyPreset; - extension_settings.quickReply.selectedPreset = name; - saveSettingsDebounced(); - loadSettings('init'); - addQuickReplyBar(); - moduleWorker(); - - $(`#quickReplyPresets option[value="${name}"]`).prop('selected', true); - console.debug('QR Preset applied: ' + name); -} - -async function doQRPresetSwitch(_, text) { - text = String(text); - applyQuickReplyPreset(text); -} - -async function doQR(_, text) { - if (!text) { - toastr.warning('must specify which QR # to use'); - return; - } - - text = Number(text); - //use scale starting with 0 - //ex: user inputs "/qr 2" >> qr with data-index 1 (but 2nd item displayed) gets triggered - let QRnum = Number(text - 1); - if (QRnum <= 0) { QRnum = 0; } - const whichQR = $('#quickReplies').find(`[data-index='${QRnum}']`); - whichQR.trigger('click'); -} - -function saveQROrder() { - //update html-level order data to match new sort - let i = 1; - $('#quickReplyContainer').children().each(function () { - const oldOrder = $(this).attr('data-order'); - $(this).attr('data-order', i); - $(this).find('input').attr('id', `quickReply${i}Label`); - $(this).find('textarea').attr('id', `quickReply${i}Mes`); - $(this).find(`#quickReply${oldOrder}CtxButton`).attr('id', `quickReply${i}CtxButton`); - $(this).find(`#quickReply${oldOrder}ExpandButton`).attr({ 'data-for': `quickReply${i}Mes`, 'id': `quickReply${i}ExpandButton` }); - i++; - }); - - //rebuild the extension_Settings array based on new order - i = 1; - $('#quickReplyContainer').children().each(function () { - onQuickReplyContextMenuChange(i); - onQuickReplyLabelInput(i); - onQuickReplyInput(i); - i++; - }); -} - -async function qrCreateCallback(args, mes) { - const qr = { - label: args.label ?? '', - mes: (mes ?? '') - .replace(/\\\|/g, '|') - .replace(/\\\{/g, '{') - .replace(/\\\}/g, '}') - , - title: args.title ?? '', - autoExecute_chatLoad: JSON.parse(args.load ?? false), - autoExecute_userMessage: JSON.parse(args.user ?? false), - autoExecute_botMessage: JSON.parse(args.bot ?? false), - autoExecute_appStartup: JSON.parse(args.startup ?? false), - hidden: JSON.parse(args.hidden ?? false), - }; - const setName = args.set ?? selected_preset; - const preset = presets.find(x => x.name == setName); - - if (!preset) { - toastr.warning('Confirm you are using proper case sensitivity!', `QR preset '${setName}' not found`); - return ''; - } - - preset.quickReplySlots.push(qr); - preset.numberOfSlots++; - await fetch('/savequickreply', { - method: 'POST', - headers: getRequestHeaders(), - body: JSON.stringify(preset), - }); - saveSettingsDebounced(); - await delay(400); - applyQuickReplyPreset(selected_preset); - return ''; -} -async function qrUpdateCallback(args, mes) { - const setName = args.set ?? selected_preset; - const preset = presets.find(x => x.name == setName); - - if (!preset) { - toastr.warning('Confirm you are using proper case sensitivity!', `QR preset '${setName}' not found`); - return ''; - } - - const idx = preset.quickReplySlots.findIndex(x => x.label == args.label); - const oqr = preset.quickReplySlots[idx]; - const qr = { - label: args.newlabel ?? oqr.label ?? '', - mes: (mes ?? oqr.mes) - .replace('\\|', '|') - .replace('\\{', '{') - .replace('\\}', '}') - , - title: args.title ?? oqr.title ?? '', - autoExecute_chatLoad: JSON.parse(args.load ?? oqr.autoExecute_chatLoad ?? false), - autoExecute_userMessage: JSON.parse(args.user ?? oqr.autoExecute_userMessage ?? false), - autoExecute_botMessage: JSON.parse(args.bot ?? oqr.autoExecute_botMessage ?? false), - autoExecute_appStartup: JSON.parse(args.startup ?? oqr.autoExecute_appStartup ?? false), - hidden: JSON.parse(args.hidden ?? oqr.hidden ?? false), - }; - preset.quickReplySlots[idx] = qr; - await fetch('/savequickreply', { - method: 'POST', - headers: getRequestHeaders(), - body: JSON.stringify(preset), - }); - saveSettingsDebounced(); - await delay(400); - applyQuickReplyPreset(selected_preset); - return ''; -} -async function qrDeleteCallback(args, label) { - const setName = args.set ?? selected_preset; - const preset = presets.find(x => x.name == setName); - - if (!preset) { - toastr.warning('Confirm you are using proper case sensitivity!', `QR preset '${setName}' not found`); - return ''; - } - - const idx = preset.quickReplySlots.findIndex(x => x.label == label); - if (idx === -1) { - toastr.warning('Confirm you are using proper case sensitivity!', `QR with label '${label}' not found`); - return ''; - }; - preset.quickReplySlots.splice(idx, 1); - preset.numberOfSlots--; - await fetch('/savequickreply', { - method: 'POST', - headers: getRequestHeaders(), - body: JSON.stringify(preset), - }); - saveSettingsDebounced(); - await delay(400); - applyQuickReplyPreset(selected_preset); - return ''; -} - -async function qrContextAddCallback(args, presetName) { - const setName = args.set ?? selected_preset; - const preset = presets.find(x => x.name == setName); - - if (!preset) { - toastr.warning('Confirm you are using proper case sensitivity!', `QR preset '${setName}' not found`); - return ''; - } - - const idx = preset.quickReplySlots.findIndex(x => x.label == args.label); - const oqr = preset.quickReplySlots[idx]; - if (!oqr.contextMenu) { - oqr.contextMenu = []; - } - let item = oqr.contextMenu.find(it => it.preset == presetName); - if (item) { - item.chain = JSON.parse(args.chain ?? 'null') ?? item.chain ?? false; - } else { - oqr.contextMenu.push({ preset: presetName, chain: JSON.parse(args.chain ?? 'false') }); - } - await fetch('/savequickreply', { - method: 'POST', - headers: getRequestHeaders(), - body: JSON.stringify(preset), - }); - saveSettingsDebounced(); - await delay(400); - applyQuickReplyPreset(selected_preset); - return ''; -} -async function qrContextDeleteCallback(args, presetName) { - const setName = args.set ?? selected_preset; - const preset = presets.find(x => x.name == setName); - - if (!preset) { - toastr.warning('Confirm you are using proper case sensitivity!', `QR preset '${setName}' not found`); - return ''; - } - - const idx = preset.quickReplySlots.findIndex(x => x.label == args.label); - const oqr = preset.quickReplySlots[idx]; - if (!oqr.contextMenu) return; - const ctxIdx = oqr.contextMenu.findIndex(it => it.preset == presetName); - if (ctxIdx > -1) { - oqr.contextMenu.splice(ctxIdx, 1); - } - await fetch('/savequickreply', { - method: 'POST', - headers: getRequestHeaders(), - body: JSON.stringify(preset), - }); - saveSettingsDebounced(); - await delay(400); - applyQuickReplyPreset(selected_preset); - return ''; -} -async function qrContextClearCallback(args, label) { - const setName = args.set ?? selected_preset; - const preset = presets.find(x => x.name == setName); - - if (!preset) { - toastr.warning('Confirm you are using proper case sensitivity!', `QR preset '${setName}' not found`); - return ''; - } - - const idx = preset.quickReplySlots.findIndex(x => x.label == label); - const oqr = preset.quickReplySlots[idx]; - oqr.contextMenu = []; - await fetch('/savequickreply', { - method: 'POST', - headers: getRequestHeaders(), - body: JSON.stringify(preset), - }); - saveSettingsDebounced(); - await delay(400); - applyQuickReplyPreset(selected_preset); - return ''; -} - -async function qrPresetAddCallback(args, name) { - const quickReplyPreset = { - name: name, - quickReplyEnabled: JSON.parse(args.enabled ?? null) ?? true, - quickActionEnabled: JSON.parse(args.nosend ?? null) ?? false, - placeBeforeInputEnabled: JSON.parse(args.before ?? null) ?? false, - quickReplySlots: [], - numberOfSlots: Number(args.slots ?? '0'), - AutoInputInject: JSON.parse(args.inject ?? 'false'), - }; - - await fetch('/savequickreply', { - method: 'POST', - headers: getRequestHeaders(), - body: JSON.stringify(quickReplyPreset), - }); - await updateQuickReplyPresetList(); -} - -async function qrPresetUpdateCallback(args, name) { - const preset = presets.find(it => it.name == name); - const quickReplyPreset = { - name: preset.name, - quickReplyEnabled: JSON.parse(args.enabled ?? null) ?? preset.quickReplyEnabled, - quickActionEnabled: JSON.parse(args.nosend ?? null) ?? preset.quickActionEnabled, - placeBeforeInputEnabled: JSON.parse(args.before ?? null) ?? preset.placeBeforeInputEnabled, - quickReplySlots: preset.quickReplySlots, - numberOfSlots: Number(args.slots ?? preset.numberOfSlots), - AutoInputInject: JSON.parse(args.inject ?? 'null') ?? preset.AutoInputInject, - }; - Object.assign(preset, quickReplyPreset); - - await fetch('/savequickreply', { - method: 'POST', - headers: getRequestHeaders(), - body: JSON.stringify(quickReplyPreset), - }); -} - -let onMessageSentExecuting = false; -let onMessageReceivedExecuting = false; -let onChatChangedExecuting = false; - -/** - * Executes quick replies on message received. - * @param {number} index New message index - * @returns {Promise} - */ -async function onMessageReceived(index) { - if (!extension_settings.quickReply.quickReplyEnabled) return; - - if (onMessageReceivedExecuting) return; - - try { - onMessageReceivedExecuting = true; - for (let i = 0; i < extension_settings.quickReply.numberOfSlots; i++) { - const qr = extension_settings.quickReply.quickReplySlots[i]; - if (qr?.autoExecute_botMessage) { - const message = getContext().chat[index]; - if (message?.mes && message?.mes !== '...') { - await sendQuickReply(i); - } - } - } - } finally { - onMessageReceivedExecuting = false; - } -} - -/** - * Executes quick replies on message sent. - * @param {number} index New message index - * @returns {Promise} - */ -async function onMessageSent(index) { - if (!extension_settings.quickReply.quickReplyEnabled) return; - - if (onMessageSentExecuting) return; - - try { - onMessageSentExecuting = true; - for (let i = 0; i < extension_settings.quickReply.numberOfSlots; i++) { - const qr = extension_settings.quickReply.quickReplySlots[i]; - if (qr?.autoExecute_userMessage) { - const message = getContext().chat[index]; - if (message?.mes && message?.mes !== '...') { - await sendQuickReply(i); - } - } - } - } finally { - onMessageSentExecuting = false; - } -} - -/** - * Executes quick replies on chat changed. - * @param {string} chatId New chat id - * @returns {Promise} - */ -async function onChatChanged(chatId) { - if (!extension_settings.quickReply.quickReplyEnabled) return; - - if (onChatChangedExecuting) return; - - try { - onChatChangedExecuting = true; - for (let i = 0; i < extension_settings.quickReply.numberOfSlots; i++) { - const qr = extension_settings.quickReply.quickReplySlots[i]; - if (qr?.autoExecute_chatLoad && chatId) { - await sendQuickReply(i); - } - } - } finally { - onChatChangedExecuting = false; - } -} - -/** - * Executes quick replies on app ready. - * @returns {Promise} - */ -async function onAppReady() { - if (!extension_settings.quickReply.quickReplyEnabled) return; - - for (let i = 0; i < extension_settings.quickReply.numberOfSlots; i++) { - const qr = extension_settings.quickReply.quickReplySlots[i]; - if (qr?.autoExecute_appStartup) { - await sendQuickReply(i); - } - } -} - -jQuery(async () => { - moduleWorker(); - setInterval(moduleWorker, UPDATE_INTERVAL); - const settingsHtml = ` -
-
-
- Quick Reply -
-
-
-
- - - - - -
- - - -
- -
-
- - -
- Customize your Quick Replies:
-
-
-
-
`; - - $('#extensions_settings2').append(settingsHtml); - - // Add event handler for quickActionEnabled - $('#quickActionEnabled').on('input', onQuickActionEnabledInput); - $('#placeBeforeInputEnabled').on('input', onPlaceBeforeInputEnabledInput); - $('#AutoInputInject').on('input', onAutoInputInject); - $('#quickReplyEnabled').on('input', onQuickReplyEnabledInput); - $('#quickReplyNumberOfSlotsApply').on('click', onQuickReplyNumberOfSlotsInput); - $('#quickReplyPresetSaveButton').on('click', saveQuickReplyPreset); - $('#quickReplyPresetUpdateButton').on('click', updateQuickReplyPreset); - - $('#quickReplyContainer').sortable({ - delay: getSortableDelay(), - stop: saveQROrder, - }); - - $('#quickReplyPresets').on('change', async function () { - const quickReplyPresetSelected = $(this).find(':selected').val(); - extension_settings.quickReplyPreset = quickReplyPresetSelected; - applyQuickReplyPreset(quickReplyPresetSelected); - saveSettingsDebounced(); - }); - - await loadSettings('init'); - addQuickReplyBar(); - - eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, onMessageReceived); - eventSource.on(event_types.USER_MESSAGE_RENDERED, onMessageSent); - eventSource.on(event_types.CHAT_CHANGED, onChatChanged); - eventSource.on(event_types.APP_READY, onAppReady); -}); - -jQuery(() => { - registerSlashCommand('qr', doQR, [], '(number) – activates the specified Quick Reply', true, true); - registerSlashCommand('qrset', doQRPresetSwitch, [], '(name) – swaps to the specified Quick Reply Preset', true, true); - const qrArgs = ` - label - string - text on the button, e.g., label=MyButton - set - string - name of the QR set, e.g., set=PresetName1 - hidden - bool - whether the button should be hidden, e.g., hidden=true - startup - bool - auto execute on app startup, e.g., startup=true - user - bool - auto execute on user message, e.g., user=true - bot - bool - auto execute on AI message, e.g., bot=true - load - bool - auto execute on chat load, e.g., load=true - title - bool - title / tooltip to be shown on button, e.g., title="My Fancy Button" - `.trim(); - const qrUpdateArgs = ` - newlabel - string - new text fort the button, e.g. newlabel=MyRenamedButton - ${qrArgs} - `.trim(); - registerSlashCommand('qr-create', qrCreateCallback, [], `(arguments [message])\n arguments:\n ${qrArgs} – creates a new Quick Reply, example: /qr-create set=MyPreset label=MyButton /echo 123`, true, true); - registerSlashCommand('qr-update', qrUpdateCallback, [], `(arguments [message])\n arguments:\n ${qrUpdateArgs} – updates Quick Reply, example: /qr-update set=MyPreset label=MyButton newlabel=MyRenamedButton /echo 123`, true, true); - registerSlashCommand('qr-delete', qrDeleteCallback, [], '(set=string [label]) – deletes Quick Reply', true, true); - registerSlashCommand('qr-contextadd', qrContextAddCallback, [], '(set=string label=string chain=bool [preset name]) – add context menu preset to a QR, example: /qr-contextadd set=MyPreset label=MyButton chain=true MyOtherPreset', true, true); - registerSlashCommand('qr-contextdel', qrContextDeleteCallback, [], '(set=string label=string [preset name]) – remove context menu preset from a QR, example: /qr-contextdel set=MyPreset label=MyButton MyOtherPreset', true, true); - registerSlashCommand('qr-contextclear', qrContextClearCallback, [], '(set=string [label]) – remove all context menu presets from a QR, example: /qr-contextclear set=MyPreset MyButton', true, true); - const presetArgs = ` - enabled - bool - enable or disable the preset - nosend - bool - disable send / insert in user input (invalid for slash commands) - before - bool - place QR before user input - slots - int - number of slots - inject - bool - inject user input automatically (if disabled use {{input}}) - `.trim(); - registerSlashCommand('qr-presetadd', qrPresetAddCallback, [], `(arguments [label])\n arguments:\n ${presetArgs} – create a new preset (overrides existing ones), example: /qr-presetadd slots=3 MyNewPreset`, true, true); - registerSlashCommand('qr-presetupdate', qrPresetUpdateCallback, [], `(arguments [label])\n arguments:\n ${presetArgs} – update an existing preset, example: /qr-presetupdate enabled=false MyPreset`, true, true); -}); diff --git a/public/scripts/extensions/quick-reply/manifest.json b/public/scripts/extensions/quick-reply/manifest.json index e104a5761..4c773fe11 100644 --- a/public/scripts/extensions/quick-reply/manifest.json +++ b/public/scripts/extensions/quick-reply/manifest.json @@ -6,6 +6,6 @@ "js": "index.js", "css": "style.css", "author": "RossAscends#1779", - "version": "1.0.0", + "version": "2.0.0", "homePage": "https://github.com/SillyTavern/SillyTavern" -} \ No newline at end of file +} diff --git a/public/scripts/extensions/quick-reply/src/ContextMenu.js b/public/scripts/extensions/quick-reply/src/ContextMenu.js deleted file mode 100644 index ad26b6bd7..000000000 --- a/public/scripts/extensions/quick-reply/src/ContextMenu.js +++ /dev/null @@ -1,67 +0,0 @@ -/** - * @typedef {import('./MenuItem.js').MenuItem} MenuItem - */ - -export class ContextMenu { - /**@type {MenuItem[]}*/ itemList = []; - /**@type {Boolean}*/ isActive = false; - - /**@type {HTMLElement}*/ root; - /**@type {HTMLElement}*/ menu; - - - - - constructor(/**@type {MenuItem[]}*/items) { - this.itemList = items; - items.forEach(item => { - item.onExpand = () => { - items.filter(it => it != item) - .forEach(it => it.collapse()); - }; - }); - } - - render() { - if (!this.root) { - const blocker = document.createElement('div'); { - this.root = blocker; - blocker.classList.add('ctx-blocker'); - blocker.addEventListener('click', () => this.hide()); - const menu = document.createElement('ul'); { - this.menu = menu; - menu.classList.add('list-group'); - menu.classList.add('ctx-menu'); - this.itemList.forEach(it => menu.append(it.render())); - blocker.append(menu); - } - } - } - return this.root; - } - - - - - show({ clientX, clientY }) { - if (this.isActive) return; - this.isActive = true; - this.render(); - this.menu.style.bottom = `${window.innerHeight - clientY}px`; - this.menu.style.left = `${clientX}px`; - document.body.append(this.root); - } - hide() { - if (this.root) { - this.root.remove(); - } - this.isActive = false; - } - toggle(/**@type {PointerEvent}*/evt) { - if (this.isActive) { - this.hide(); - } else { - this.show(evt); - } - } -} diff --git a/public/scripts/extensions/quick-reply/src/MenuHeader.js b/public/scripts/extensions/quick-reply/src/MenuHeader.js deleted file mode 100644 index f1f38c83c..000000000 --- a/public/scripts/extensions/quick-reply/src/MenuHeader.js +++ /dev/null @@ -1,20 +0,0 @@ -import { MenuItem } from './MenuItem.js'; - -export class MenuHeader extends MenuItem { - constructor(/**@type {String}*/label) { - super(label, null, null); - } - - - render() { - if (!this.root) { - const item = document.createElement('li'); { - this.root = item; - item.classList.add('list-group-item'); - item.classList.add('ctx-header'); - item.append(this.label); - } - } - return this.root; - } -} diff --git a/public/scripts/extensions/quick-reply/src/MenuItem.js b/public/scripts/extensions/quick-reply/src/MenuItem.js deleted file mode 100644 index d72fad310..000000000 --- a/public/scripts/extensions/quick-reply/src/MenuItem.js +++ /dev/null @@ -1,76 +0,0 @@ -import { SubMenu } from './SubMenu.js'; - -export class MenuItem { - /**@type {String}*/ label; - /**@type {Object}*/ value; - /**@type {Function}*/ callback; - /**@type {MenuItem[]}*/ childList = []; - /**@type {SubMenu}*/ subMenu; - /**@type {Boolean}*/ isForceExpanded = false; - - /**@type {HTMLElement}*/ root; - - /**@type {Function}*/ onExpand; - - - - - constructor(/**@type {String}*/label, /**@type {Object}*/value, /**@type {function}*/callback, /**@type {MenuItem[]}*/children = []) { - this.label = label; - this.value = value; - this.callback = callback; - this.childList = children; - } - - - render() { - if (!this.root) { - const item = document.createElement('li'); { - this.root = item; - item.classList.add('list-group-item'); - item.classList.add('ctx-item'); - item.title = this.value; - if (this.callback) { - item.addEventListener('click', (evt) => this.callback(evt, this)); - } - item.append(this.label); - if (this.childList.length > 0) { - item.classList.add('ctx-has-children'); - const sub = new SubMenu(this.childList); - this.subMenu = sub; - const trigger = document.createElement('div'); { - trigger.classList.add('ctx-expander'); - trigger.textContent = '⋮'; - trigger.addEventListener('click', (evt) => { - evt.stopPropagation(); - this.toggle(); - }); - item.append(trigger); - } - item.addEventListener('mouseover', () => sub.show(item)); - item.addEventListener('mouseleave', () => sub.hide()); - - } - } - } - return this.root; - } - - - expand() { - this.subMenu?.show(this.root); - if (this.onExpand) { - this.onExpand(); - } - } - collapse() { - this.subMenu?.hide(); - } - toggle() { - if (this.subMenu.isActive) { - this.expand(); - } else { - this.collapse(); - } - } -} diff --git a/public/scripts/extensions/quick-reply/src/SubMenu.js b/public/scripts/extensions/quick-reply/src/SubMenu.js deleted file mode 100644 index a018c60af..000000000 --- a/public/scripts/extensions/quick-reply/src/SubMenu.js +++ /dev/null @@ -1,66 +0,0 @@ -/** - * @typedef {import('./MenuItem.js').MenuItem} MenuItem - */ - -export class SubMenu { - /**@type {MenuItem[]}*/ itemList = []; - /**@type {Boolean}*/ isActive = false; - - /**@type {HTMLElement}*/ root; - - - - - constructor(/**@type {MenuItem[]}*/items) { - this.itemList = items; - } - - render() { - if (!this.root) { - const menu = document.createElement('ul'); { - this.root = menu; - menu.classList.add('list-group'); - menu.classList.add('ctx-menu'); - menu.classList.add('ctx-sub-menu'); - this.itemList.forEach(it => menu.append(it.render())); - } - } - return this.root; - } - - - - - show(/**@type {HTMLElement}*/parent) { - if (this.isActive) return; - this.isActive = true; - this.render(); - parent.append(this.root); - requestAnimationFrame(() => { - const rect = this.root.getBoundingClientRect(); - console.log(window.innerHeight, rect); - if (rect.bottom > window.innerHeight - 5) { - this.root.style.top = `${window.innerHeight - 5 - rect.bottom}px`; - } - if (rect.right > window.innerWidth - 5) { - this.root.style.left = 'unset'; - this.root.style.right = '100%'; - } - }); - } - hide() { - if (this.root) { - this.root.remove(); - this.root.style.top = ''; - this.root.style.left = ''; - } - this.isActive = false; - } - toggle(/**@type {HTMLElement}*/parent) { - if (this.isActive) { - this.hide(); - } else { - this.show(parent); - } - } -} diff --git a/public/scripts/extensions/quick-reply/style.css b/public/scripts/extensions/quick-reply/style.css deleted file mode 100644 index 0c85d1760..000000000 --- a/public/scripts/extensions/quick-reply/style.css +++ /dev/null @@ -1,114 +0,0 @@ -#quickReplyBar { - outline: none; - /* - padding: 5px 0; - border-bottom: 1px solid var(--SmartThemeBorderColor); - */ - margin: 0; - transition: 0.3s; - opacity: 0.7; - display: flex; - align-items: center; - justify-content: center; - width: 100%; - display: none; - max-width: 100%; - overflow-x: auto; - order: 1; - position: relative; -} - -#quickReplies { - margin: 0; - padding: 0; - display: flex; - justify-content: center; - flex-wrap: wrap; - gap: 5px; - width: 100%; -} - -#quickReplyPopoutButton { - position: absolute; - right: 5px; - top: 0px; -} - -#quickReplies div { - color: var(--SmartThemeBodyColor); - background-color: var(--black50a); - border: 1px solid var(--SmartThemeBorderColor); - border-radius: 10px; - padding: 3px 5px; - margin: 3px 0; - /* width: min-content; */ - cursor: pointer; - transition: 0.3s; - display: flex; - align-items: center; - justify-content: center; - text-align: center; -} - -#quickReplies div:hover { - opacity: 1; - filter: brightness(1.2); - cursor: pointer; -} - - -.ctx-blocker { - /* backdrop-filter: blur(1px); */ - /* background-color: rgba(0 0 0 / 10%); */ - bottom: 0; - left: 0; - position: fixed; - right: 0; - top: 0; - z-index: 999; -} - -.ctx-menu { - position: absolute; - overflow: visible; -} - -.list-group .list-group-item.ctx-header { - font-weight: bold; - cursor: default; -} - -.ctx-item+.ctx-header { - border-top: 1px solid; -} - -.ctx-item { - position: relative; -} - -.ctx-expander { - border-left: 1px solid; - margin-left: 1em; - text-align: center; - width: 2em; -} - -.ctx-expander:hover { - font-weight: bold; -} - -.ctx-sub-menu { - position: absolute; - top: 0; - left: 100%; -} - -@media screen and (max-width: 1000px) { - .ctx-blocker { - position: absolute; - } - - .list-group .list-group-item.ctx-item { - padding: 1em; - } -} From 69d6b9379a80cd59083cf9031f917d576a67c69c Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Wed, 20 Dec 2023 13:40:44 +0000 Subject: [PATCH 002/522] implement QR basics --- .../quick-reply/html/qrOptions.html | 53 +++ .../extensions/quick-reply/html/settings.html | 61 ++++ .../scripts/extensions/quick-reply/index.js | 202 +++++++++++ .../extensions/quick-reply/src/QuickReply.js | 338 ++++++++++++++++++ .../quick-reply/src/QuickReplyConfig.js | 22 ++ .../quick-reply/src/QuickReplyContextLink.js | 25 ++ .../quick-reply/src/QuickReplyLink.js | 24 ++ .../quick-reply/src/QuickReplySet.js | 192 ++++++++++ .../quick-reply/src/QuickReplySettings.js | 42 +++ .../extensions/quick-reply/src/ui/ButtonUi.js | 63 ++++ .../quick-reply/src/ui/SettingsUi.js | 310 ++++++++++++++++ .../scripts/extensions/quick-reply/style.css | 114 ++++++ .../scripts/extensions/quick-reply/style.less | 124 +++++++ 13 files changed, 1570 insertions(+) create mode 100644 public/scripts/extensions/quick-reply/html/qrOptions.html create mode 100644 public/scripts/extensions/quick-reply/html/settings.html create mode 100644 public/scripts/extensions/quick-reply/index.js create mode 100644 public/scripts/extensions/quick-reply/src/QuickReply.js create mode 100644 public/scripts/extensions/quick-reply/src/QuickReplyConfig.js create mode 100644 public/scripts/extensions/quick-reply/src/QuickReplyContextLink.js create mode 100644 public/scripts/extensions/quick-reply/src/QuickReplyLink.js create mode 100644 public/scripts/extensions/quick-reply/src/QuickReplySet.js create mode 100644 public/scripts/extensions/quick-reply/src/QuickReplySettings.js create mode 100644 public/scripts/extensions/quick-reply/src/ui/ButtonUi.js create mode 100644 public/scripts/extensions/quick-reply/src/ui/SettingsUi.js create mode 100644 public/scripts/extensions/quick-reply/style.css create mode 100644 public/scripts/extensions/quick-reply/style.less diff --git a/public/scripts/extensions/quick-reply/html/qrOptions.html b/public/scripts/extensions/quick-reply/html/qrOptions.html new file mode 100644 index 000000000..78a094ef2 --- /dev/null +++ b/public/scripts/extensions/quick-reply/html/qrOptions.html @@ -0,0 +1,53 @@ +
+

Context Menu

+
+ +
+
+ +
+ + +

Auto-Execute

+
+ + + + + +
+ + +

UI Options

+
+ +
+
diff --git a/public/scripts/extensions/quick-reply/html/settings.html b/public/scripts/extensions/quick-reply/html/settings.html new file mode 100644 index 000000000..4b5d0d8b3 --- /dev/null +++ b/public/scripts/extensions/quick-reply/html/settings.html @@ -0,0 +1,61 @@ +
+
+
+ Quick Reply +
+
+
+ + + +
+ +
+
Global Quick Reply Sets
+
+ +
+
+
+ +
+ +
+
Chat Quick Reply Sets
+
+ +
+
+
+ +
+ +
+
Edit Quick Replies
+
+ + +
+
+
+ + + +
+
+
+ +
+
+
+
diff --git a/public/scripts/extensions/quick-reply/index.js b/public/scripts/extensions/quick-reply/index.js new file mode 100644 index 000000000..89eee9abf --- /dev/null +++ b/public/scripts/extensions/quick-reply/index.js @@ -0,0 +1,202 @@ +import { chat_metadata, eventSource, event_types, getRequestHeaders } from '../../../script.js'; +import { extension_settings } from '../../extensions.js'; +import { QuickReply } from './src/QuickReply.js'; +import { QuickReplyConfig } from './src/QuickReplyConfig.js'; +import { QuickReplyContextLink } from './src/QuickReplyContextLink.js'; +import { QuickReplySet } from './src/QuickReplySet.js'; +import { QuickReplySettings } from './src/QuickReplySettings.js'; +import { ButtonUi } from './src/ui/ButtonUi.js'; +import { SettingsUi } from './src/ui/SettingsUi.js'; + + + + +//TODO popout QR button bar (allow separate popouts for each QR set?) +//TODO context menus +//TODO move advanced QR options into own UI class +//TODO slash commands +//TODO create new QR set +//TODO delete QR set +//TODO easy way to CRUD QRs and sets +//TODO easy way to set global and chat sets + + + + +const _VERBOSE = true; +export const log = (...msg) => _VERBOSE ? console.log('[QR2]', ...msg) : null; +export const warn = (...msg) => _VERBOSE ? console.warn('[QR2]', ...msg) : null; + + +const defaultConfig = { + setList: [{ + set: 'Default', + isVisible: true, + }], +}; + +const defaultSettings = { + isEnabled: true, + isCombined: false, + config: defaultConfig, +}; + + +/** @type {QuickReplySettings}*/ +let settings; +/** @type {SettingsUi} */ +let manager; +/** @type {ButtonUi} */ +let buttons; + + + + +const loadSets = async () => { + const response = await fetch('/api/settings/get', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({}), + }); + + if (response.ok) { + const setList = (await response.json()).quickReplyPresets ?? []; + for (const set of setList) { + if (set.version == 2) { + QuickReplySet.list.push(QuickReplySet.from(set)); + } else { + const qrs = new QuickReplySet(); + qrs.name = set.name; + qrs.disableSend = set.quickActionEnabled ?? false; + qrs.placeBeforeInput = set.placeBeforeInputEnabled ?? false; + qrs.injectInput = set.AutoInputInject ?? false; + qrs.qrList = set.quickReplySlots.map((slot,idx)=>{ + const qr = new QuickReply(); + qr.id = idx + 1; + qr.label = slot.label; + qr.title = slot.title; + qr.message = slot.mes; + qr.isHidden = slot.hidden ?? false; + qr.executeOnStartup = slot.autoExecute_appStartup ?? false; + qr.executeOnUser = slot.autoExecute_userMessage ?? false; + qr.executeOnAi = slot.autoExecute_botMessage ?? false; + qr.executeOnChatChange = slot.autoExecute_chatLoad ?? false; + qr.contextList = (slot.contextMenu ?? []).map(it=>(QuickReplyContextLink.from({ + set: it.preset, + isChained: it.chain, + }))); + return qr; + }); + QuickReplySet.list.push(qrs); + await qrs.save(); + } + } + log('sets: ', QuickReplySet.list); + } +}; + +const loadSettings = async () => { + //TODO migrate old settings + if (!extension_settings.quickReplyV2) { + extension_settings.quickReplyV2 = defaultSettings; + } + try { + settings = QuickReplySettings.from(extension_settings.quickReplyV2); + } catch { + settings = QuickReplySettings.from(defaultSettings); + } +}; + + + + +const init = async () => { + await loadSets(); + await loadSettings(); + log('settings: ', settings); + + manager = new SettingsUi(settings); + document.querySelector('#extensions_settings2').append(await manager.render()); + + buttons = new ButtonUi(settings); + buttons.show(); + settings.onSave = ()=>buttons.refresh(); + + window['executeQuickReplyByName'] = async(name) => { + let qr = [...settings.config.setList, ...(settings.chatConfig?.setList ?? [])] + .map(it=>it.set.qrList) + .flat() + .find(it=>it.label == name) + ; + if (!qr) { + let [setName, ...qrName] = name.split('.'); + name = qrName.join('.'); + let qrs = QuickReplySet.get(setName); + if (qrs) { + qr = qrs.qrList.find(it=>it.label == name); + } + } + if (qr && qr.onExecute) { + return await qr.onExecute(); + } + }; + + if (settings.isEnabled) { + const qrList = [ + ...settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnStartup)).flat(), + ...(settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnStartup))?.flat() ?? []), + ]; + for (const qr of qrList) { + await qr.onExecute(); + } + } +}; +eventSource.on(event_types.APP_READY, init); + +const onChatChanged = async (chatIdx) => { + log('CHAT_CHANGED', chatIdx); + if (chatIdx) { + settings.chatConfig = QuickReplyConfig.from(chat_metadata.quickReply ?? {}); + } else { + settings.chatConfig = null; + } + manager.rerender(); + buttons.refresh(); + + if (settings.isEnabled) { + const qrList = [ + ...settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnChatChange)).flat(), + ...(settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnChatChange))?.flat() ?? []), + ]; + for (const qr of qrList) { + await qr.onExecute(); + } + } +}; +eventSource.on(event_types.CHAT_CHANGED, onChatChanged); + +const onUserMessage = async () => { + if (settings.isEnabled) { + const qrList = [ + ...settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnUser)).flat(), + ...(settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnUser))?.flat() ?? []), + ]; + for (const qr of qrList) { + await qr.onExecute(); + } + } +}; +eventSource.on(event_types.USER_MESSAGE_RENDERED, onUserMessage); + +const onAiMessage = async () => { + if (settings.isEnabled) { + const qrList = [ + ...settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnAi)).flat(), + ...(settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnAi))?.flat() ?? []), + ]; + for (const qr of qrList) { + await qr.onExecute(); + } + } +}; +eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, onAiMessage); diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js new file mode 100644 index 000000000..4e27d63aa --- /dev/null +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -0,0 +1,338 @@ +import { callPopup } from '../../../../script.js'; +import { getSortableDelay } from '../../../utils.js'; +import { log, warn } from '../index.js'; +import { QuickReplyContextLink } from './QuickReplyContextLink.js'; +import { QuickReplySet } from './QuickReplySet.js'; + +export class QuickReply { + /** + * @param {{ id?: number; contextList?: any; }} props + */ + static from(props) { + props.contextList = (props.contextList ?? []).map((/** @type {any} */ it)=>QuickReplyContextLink.from(it)); + return Object.assign(new this(), props); + } + + + + + /**@type {Number}*/ id; + /**@type {String}*/ label = ''; + /**@type {String}*/ title = ''; + /**@type {String}*/ message = ''; + + /**@type {QuickReplyContextLink[]}*/ contextList; + + /**@type {Boolean}*/ isHidden = false; + /**@type {Boolean}*/ executeOnStartup = false; + /**@type {Boolean}*/ executeOnUser = false; + /**@type {Boolean}*/ executeOnAi = false; + /**@type {Boolean}*/ executeOnChatChange = false; + + /**@type {Function}*/ onExecute; + /**@type {Function}*/ onDelete; + /**@type {Function}*/ onUpdate; + + + /**@type {HTMLElement}*/ dom; + /**@type {HTMLElement}*/ settingsDom; + + + + + unrender() { + this.dom?.remove(); + this.dom = null; + } + updateRender() { + if (!this.dom) return; + this.dom.title = this.title || this.message; + this.dom.textContent = this.label; + } + render() { + this.unrender(); + if (!this.dom) { + const root = document.createElement('div'); { + this.dom = root; + root.classList.add('qr--button'); + root.title = this.title || this.message; + root.textContent = this.label; + root.addEventListener('click', ()=>{ + if (this.message?.length > 0 && this.onExecute) { + this.onExecute(this); + } + }); + } + } + return this.dom; + } + + + + + /** + * @param {any} idx + */ + renderSettings(idx) { + if (!this.settingsDom) { + const item = document.createElement('div'); { + this.settingsDom = item; + item.classList.add('qr--set-item'); + item.setAttribute('data-order', String(idx)); + item.setAttribute('data-id', String(this.id)); + const drag = document.createElement('div'); { + drag.classList.add('drag-handle'); + drag.classList.add('ui-sortable-handle'); + drag.textContent = '☰'; + item.append(drag); + } + const lblContainer = document.createElement('div'); { + lblContainer.classList.add('qr--set-itemLabelContainer'); + const lbl = document.createElement('input'); { + lbl.classList.add('qr--set-itemLabel'); + lbl.classList.add('text_pole'); + lbl.value = this.label; + lbl.addEventListener('input', ()=>this.updateLabel(lbl.value)); + lblContainer.append(lbl); + } + item.append(lblContainer); + } + const optContainer = document.createElement('div'); { + optContainer.classList.add('qr--set-optionsContainer'); + const opt = document.createElement('div'); { + opt.classList.add('qr--action'); + opt.classList.add('menu_button'); + opt.classList.add('fa-solid'); + opt.textContent = '⁝'; + opt.title = 'Additional options:\n - context menu\n - auto-execution\n - tooltip'; + opt.addEventListener('click', ()=>this.showOptions()); + optContainer.append(opt); + } + item.append(optContainer); + } + const expandContainer = document.createElement('div'); { + expandContainer.classList.add('qr--set-optionsContainer'); + const expand = document.createElement('div'); { + expand.classList.add('qr--expand'); + expand.classList.add('menu_button'); + expand.classList.add('menu_button_icon'); + expand.classList.add('editor_maximize'); + expand.classList.add('fa-solid'); + expand.classList.add('fa-maximize'); + expand.title = 'Expand the editor'; + expand.setAttribute('data-for', `qr--set--item${this.id}`); + expand.setAttribute('data-tab', 'true'); + expandContainer.append(expand); + } + item.append(expandContainer); + } + const mes = document.createElement('textarea'); { + mes.id = `qr--set--item${this.id}`; + mes.classList.add('qr--set-itemMessage'); + mes.value = this.message; + //HACK need to use jQuery to catch the triggered event from the expanded editor + $(mes).on('input', ()=>this.updateMessage(mes.value)); + item.append(mes); + } + const actions = document.createElement('div'); { + actions.classList.add('qr--actions'); + const del = document.createElement('div'); { + del.classList.add('qr--action'); + del.classList.add('menu_button'); + del.classList.add('menu_button_icon'); + del.classList.add('fa-solid'); + del.classList.add('fa-trash-can'); + del.title = 'Remove quick reply'; + del.addEventListener('click', ()=>this.delete()); + actions.append(del); + } + item.append(actions); + } + } + } + return this.settingsDom; + } + unrenderSettings() { + this.settingsDom?.remove(); + } + + + + + delete() { + if (this.onDelete) { + this.unrender(); + this.unrenderSettings(); + this.onDelete(this); + } + } + /** + * @param {string} value + */ + updateMessage(value) { + if (this.onUpdate) { + this.message = value; + this.updateRender(); + this.onUpdate(this); + } + } + /** + * @param {string} value + */ + updateLabel(value) { + if (this.onUpdate) { + this.label = value; + this.updateRender(); + this.onUpdate(this); + } + } + + updateContext() { + if (this.onUpdate) { + this.updateRender(); + this.onUpdate(this); + } + } + + async showOptions() { + const response = await fetch('/scripts/extensions/quick-reply/html/qrOptions.html', { cache: 'no-store' }); + if (response.ok) { + this.template = document.createRange().createContextualFragment(await response.text()).querySelector('#qr--qrOptions'); + /**@type {HTMLElement} */ + // @ts-ignore + const dom = this.template.cloneNode(true); + const popupResult = callPopup(dom, 'text', undefined, { okButton: 'OK', wide: false, large: false, rows: 1 }); + /**@type {HTMLTemplateElement}*/ + const tpl = dom.querySelector('#qr--ctxItem'); + const linkList = dom.querySelector('#qr--ctxEditor'); + const fillQrSetSelect = (/**@type {HTMLSelectElement}*/select, /**@type {QuickReplyContextLink}*/ link) => { + [{ name: 'Select a QR set' }, ...QuickReplySet.list].forEach(qrs => { + const opt = document.createElement('option'); { + opt.value = qrs.name; + opt.textContent = qrs.name; + opt.selected = qrs.name == link.set?.name; + select.append(opt); + } + }); + }; + const addCtxItem = (/**@type {QuickReplyContextLink}*/link, /**@type {Number}*/idx) => { + /**@type {HTMLElement} */ + // @ts-ignore + const itemDom = tpl.content.querySelector('.qr--ctxItem').cloneNode(true); { + itemDom.setAttribute('data-order', String(idx)); + + /**@type {HTMLSelectElement} */ + const select = itemDom.querySelector('.qr--set'); + fillQrSetSelect(select, link); + select.addEventListener('change', () => { + link.set = QuickReplySet.get(select.value); + this.updateContext(); + }); + + /**@type {HTMLInputElement} */ + const chain = itemDom.querySelector('.qr--isChained'); + chain.checked = link.isChained; + chain.addEventListener('click', () => { + link.isChained = chain.checked; + this.updateContext(); + }); + + itemDom.querySelector('.qr--delete').addEventListener('click', () => { + itemDom.remove(); + this.contextList.splice(this.contextList.indexOf(link), 1); + this.updateContext(); + }); + + linkList.append(itemDom); + } + }; + [...this.contextList].forEach((link, idx) => addCtxItem(link, idx)); + dom.querySelector('#qr--ctxAdd').addEventListener('click', () => { + const link = new QuickReplyContextLink(); + this.contextList.push(link); + addCtxItem(link, this.contextList.length - 1); + }); + const onContextSort = () => { + this.contextList = Array.from(linkList.querySelectorAll('.qr--ctxItem')).map((it,idx) => { + const link = this.contextList[Number(it.getAttribute('data-order'))]; + it.setAttribute('data-order', String(idx)); + return link; + }); + this.updateContext(); + }; + // @ts-ignore + $(linkList).sortable({ + delay: getSortableDelay(), + stop: () => onContextSort(), + }); + + // auto-exec + /**@type {HTMLInputElement}*/ + const isHidden = dom.querySelector('#qr--isHidden'); + isHidden.checked = this.isHidden; + isHidden.addEventListener('click', ()=>{ + this.isHidden = isHidden.checked; + this.updateContext(); + }); + /**@type {HTMLInputElement}*/ + const executeOnStartup = dom.querySelector('#qr--executeOnStartup'); + executeOnStartup.checked = this.executeOnStartup; + executeOnStartup.addEventListener('click', ()=>{ + this.executeOnStartup = executeOnStartup.checked; + this.updateContext(); + }); + /**@type {HTMLInputElement}*/ + const executeOnUser = dom.querySelector('#qr--executeOnUser'); + executeOnUser.checked = this.executeOnUser; + executeOnUser.addEventListener('click', ()=>{ + this.executeOnUser = executeOnUser.checked; + this.updateContext(); + }); + /**@type {HTMLInputElement}*/ + const executeOnAi = dom.querySelector('#qr--executeOnAi'); + executeOnAi.checked = this.executeOnAi; + executeOnAi.addEventListener('click', ()=>{ + this.executeOnAi = executeOnAi.checked; + this.updateContext(); + }); + /**@type {HTMLInputElement}*/ + const executeOnChatChange = dom.querySelector('#qr--executeOnChatChange'); + executeOnChatChange.checked = this.executeOnChatChange; + executeOnChatChange.addEventListener('click', ()=>{ + this.executeOnChatChange = executeOnChatChange.checked; + this.updateContext(); + }); + + // UI options + /**@type {HTMLInputElement}*/ + const title = dom.querySelector('#qr--title'); + title.value = this.title; + title.addEventListener('input', () => { + this.title = title.value.trim(); + this.updateContext(); + }); + + await popupResult; + } else { + warn('failed to fetch qrOptions template'); + } + } + + + + + toJSON() { + return { + id: this.id, + label: this.label, + title: this.title, + message: this.message, + contextList: this.contextList, + isHidden: this.isHidden, + executeOnStartup: this.executeOnStartup, + executeOnUser: this.executeOnUser, + executeOnAi: this.executeOnAi, + executeOnChatChange: this.executeOnChatChange, + }; + } +} diff --git a/public/scripts/extensions/quick-reply/src/QuickReplyConfig.js b/public/scripts/extensions/quick-reply/src/QuickReplyConfig.js new file mode 100644 index 000000000..701d00be6 --- /dev/null +++ b/public/scripts/extensions/quick-reply/src/QuickReplyConfig.js @@ -0,0 +1,22 @@ +import { QuickReplyLink } from './QuickReplyLink.js'; + +export class QuickReplyConfig { + /**@type {QuickReplyLink[]}*/ setList = []; + + + + + static from(props) { + props.setList = props.setList?.map(it=>QuickReplyLink.from(it)) ?? []; + return Object.assign(new this(), props); + } + + + + + toJSON() { + return { + setList: this.setList, + }; + } +} diff --git a/public/scripts/extensions/quick-reply/src/QuickReplyContextLink.js b/public/scripts/extensions/quick-reply/src/QuickReplyContextLink.js new file mode 100644 index 000000000..2c1d1e51f --- /dev/null +++ b/public/scripts/extensions/quick-reply/src/QuickReplyContextLink.js @@ -0,0 +1,25 @@ +import { QuickReplySet } from './QuickReplySet.js'; + +export class QuickReplyContextLink { + static from(props) { + props.set = QuickReplySet.get(props.set); + const x = Object.assign(new this(), props); + return x; + } + + + + + /**@type {QuickReplySet}*/ set; + /**@type {Boolean}*/ isChained = false; + + + + + toJSON() { + return { + set: this.set?.name, + isChained: this.isChained, + }; + } +} diff --git a/public/scripts/extensions/quick-reply/src/QuickReplyLink.js b/public/scripts/extensions/quick-reply/src/QuickReplyLink.js new file mode 100644 index 000000000..05d37f1b4 --- /dev/null +++ b/public/scripts/extensions/quick-reply/src/QuickReplyLink.js @@ -0,0 +1,24 @@ +import { QuickReplySet } from './QuickReplySet.js'; + +export class QuickReplyLink { + /**@type {QuickReplySet}*/ set; + /**@type {Boolean}*/ isVisible = true; + + + + + static from(props) { + props.set = QuickReplySet.get(props.set); + return Object.assign(new this(), props); + } + + + + + toJSON() { + return { + set: this.set.name, + isVisible: this.isVisible, + }; + } +} diff --git a/public/scripts/extensions/quick-reply/src/QuickReplySet.js b/public/scripts/extensions/quick-reply/src/QuickReplySet.js new file mode 100644 index 000000000..5fb534f0f --- /dev/null +++ b/public/scripts/extensions/quick-reply/src/QuickReplySet.js @@ -0,0 +1,192 @@ +import { getRequestHeaders } from '../../../../script.js'; +import { executeSlashCommands } from '../../../slash-commands.js'; +import { debounce } from '../../../utils.js'; +import { warn } from '../index.js'; +import { QuickReply } from './QuickReply.js'; + +export class QuickReplySet { + /**@type {QuickReplySet[]}*/ static list = []; + + + static from(props) { + props.qrList = props.qrList?.map(it=>QuickReply.from(it)); + const instance = Object.assign(new this(), props); + instance.init(); + return instance; + } + + /** + * @param {String} name - name of the QuickReplySet + */ + static get(name) { + return this.list.find(it=>it.name == name); + } + + + + + /**@type {String}*/ name; + + /**@type {Boolean}*/ disableSend = false; + /**@type {Boolean}*/ placeBeforeInput = false; + /**@type {Boolean}*/ injectInput = false; + + /**@type {QuickReply[]}*/ qrList = []; + + /**@type {Number}*/ idIndex = 0; + + /**@type {Function}*/ save; + + + /**@type {HTMLElement}*/ dom; + /**@type {HTMLElement}*/ settingsDom; + + + + + constructor() { + this.save = debounce(()=>this.performSave(), 200); + } + + init() { + this.qrList.forEach(qr=>this.hookQuickReply(qr)); + } + + + + + unrender() { + this.dom?.remove(); + this.dom = null; + } + render() { + this.unrender(); + if (!this.dom) { + const root = document.createElement('div'); { + this.dom = root; + root.classList.add('qr--buttons'); + this.qrList.filter(qr=>!qr.isHidden).forEach(qr=>{ + root.append(qr.render()); + }); + } + } + return this.dom; + } + rerender() { + if (!this.dom) return; + this.dom.innerHTML = ''; + this.qrList.filter(qr=>!qr.isHidden).forEach(qr=>{ + this.dom.append(qr.render()); + }); + } + + + + + renderSettings() { + if (!this.settingsDom) { + this.settingsDom = document.createElement('div'); { + this.settingsDom.classList.add('qr--set-qrListContents'); + this.qrList.forEach((qr,idx)=>{ + this.renderSettingsItem(qr, idx); + }); + } + } + return this.settingsDom; + } + renderSettingsItem(qr, idx) { + this.settingsDom.append(qr.renderSettings(idx)); + } + + + + + /** + * @param {QuickReply} qr + */ + async execute(qr) { + /**@type {HTMLTextAreaElement}*/ + const ta = document.querySelector('#send_textarea'); + let input = ta.value; + if (this.injectInput && input.length > 0) { + if (this.placeBeforeInput) { + input = `${qr.message} ${input}`; + } else { + input = `${input} ${qr.message}`; + } + } else { + input = `${qr.message} `; + } + + if (input[0] == '/' && !this.disableSend) { + const result = await executeSlashCommands(input); + return typeof result === 'object' ? result?.pipe : ''; + } + + ta.value = input; + ta.focus(); + + if (!this.disableSend) { + // @ts-ignore + document.querySelector('#send_but').click(); + } + } + + + + + addQuickReply() { + const id = Math.max(this.idIndex, this.qrList.reduce((max,qr)=>Math.max(max,qr.id),0)) + 1; + this.idIndex = id + 1; + const qr = QuickReply.from({ id }); + this.qrList.push(qr); + this.hookQuickReply(qr); + if (this.settingsDom) { + this.renderSettingsItem(qr, this.qrList.length - 1); + } + if (this.dom) { + this.dom.append(qr.render()); + } + this.save(); + return qr; + } + + hookQuickReply(qr) { + qr.onExecute = ()=>this.execute(qr); + qr.onDelete = ()=>this.removeQuickReply(qr); + qr.onUpdate = ()=>this.save(); + } + + removeQuickReply(qr) { + this.qrList.splice(this.qrList.indexOf(qr), 1); + this.save(); + } + + + toJSON() { + return { + version: 2, + name: this.name, + disableSend: this.disableSend, + placeBeforeInput: this.placeBeforeInput, + injectInput: this.injectInput, + qrList: this.qrList, + idIndex: this.idIndex, + }; + } + + + async performSave() { + const response = await fetch('/savequickreply', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify(this), + }); + + if (response.ok) { + this.rerender(); + } else { + warn(`Failed to save Quick Reply Set: ${this.name}`); + } + } +} diff --git a/public/scripts/extensions/quick-reply/src/QuickReplySettings.js b/public/scripts/extensions/quick-reply/src/QuickReplySettings.js new file mode 100644 index 000000000..c2052e911 --- /dev/null +++ b/public/scripts/extensions/quick-reply/src/QuickReplySettings.js @@ -0,0 +1,42 @@ +import { chat_metadata, saveChatDebounced, saveSettingsDebounced } from '../../../../script.js'; +import { extension_settings } from '../../../extensions.js'; +import { QuickReplyConfig } from './QuickReplyConfig.js'; + +export class QuickReplySettings { + static from(props) { + props.config = QuickReplyConfig.from(props.config); + return Object.assign(new this(), props); + } + + + + + /**@type {Boolean}*/ isEnabled = false; + /**@type {Boolean}*/ isCombined = false; + /**@type {QuickReplyConfig}*/ config; + /**@type {QuickReplyConfig}*/ chatConfig; + + /**@type {Function}*/ onSave; + + + + + save() { + extension_settings.quickReplyV2 = this.toJSON(); + saveSettingsDebounced(); + if (this.chatConfig) { + chat_metadata.quickReply = this.chatConfig.toJSON(); + saveChatDebounced(); + } + if (this.onSave) { + this.onSave(); + } + } + + toJSON() { + return { + isEnabled: this.isEnabled, + config: this.config, + }; + } +} diff --git a/public/scripts/extensions/quick-reply/src/ui/ButtonUi.js b/public/scripts/extensions/quick-reply/src/ui/ButtonUi.js new file mode 100644 index 000000000..ede8a553f --- /dev/null +++ b/public/scripts/extensions/quick-reply/src/ui/ButtonUi.js @@ -0,0 +1,63 @@ +import { QuickReplySettings } from '../QuickReplySettings.js'; + +export class ButtonUi { + /**@type {QuickReplySettings}*/ settings; + + /**@type {HTMLElement}*/ dom; + + + + + constructor(/**@type {QuickReplySettings}*/settings) { + this.settings = settings; + } + + + + + render() { + if (!this.dom) { + let buttonHolder; + const root = document.createElement('div'); { + this.dom = root; + buttonHolder = root; + root.id = 'qr--bar'; + root.classList.add('flex-container'); + root.classList.add('flexGap5'); + if (this.settings.isCombined) { + const buttons = document.createElement('div'); { + buttonHolder = buttons; + buttons.classList.add('qr--buttons'); + root.append(buttons); + } + } + [...this.settings.config.setList, ...(this.settings.chatConfig?.setList ?? [])] + .filter(link=>link.isVisible) + .forEach(link=>buttonHolder.append(link.set.render())) + ; + } + } + return this.dom; + } + unrender() { + this.dom?.remove(); + this.dom = null; + } + + show() { + if (!this.settings.isEnabled) return; + const sendForm = document.querySelector('#send_form'); + if (sendForm.children.length > 0) { + sendForm.children[0].insertAdjacentElement('beforebegin', this.render()); + } else { + sendForm.append(this.render()); + } + } + hide() { + this.unrender(); + } + refresh() { + this.hide(); + this.show(); + } +} diff --git a/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js b/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js new file mode 100644 index 000000000..dae46345f --- /dev/null +++ b/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js @@ -0,0 +1,310 @@ +import { getSortableDelay } from '../../../../utils.js'; +import { warn } from '../../index.js'; +import { QuickReplyLink } from '../QuickReplyLink.js'; +import { QuickReplySet } from '../QuickReplySet.js'; +// eslint-disable-next-line no-unused-vars +import { QuickReplySettings } from '../QuickReplySettings.js'; + +export class SettingsUi { + /**@type {QuickReplySettings}*/ settings; + + /**@type {HTMLElement}*/ template; + /**@type {HTMLElement}*/ dom; + + /**@type {HTMLInputElement}*/ isEnabled; + /**@type {HTMLInputElement}*/ isCombined; + + /**@type {HTMLElement}*/ globalSetList; + + /**@type {HTMLElement}*/ chatSetList; + + /**@type {QuickReplySet}*/ currentQrSet; + /**@type {HTMLInputElement}*/ disableSend; + /**@type {HTMLInputElement}*/ placeBeforeInput; + /**@type {HTMLInputElement}*/ injectInput; + /**@type {HTMLSelectElement}*/ currentSet; + + + + + constructor(/**@type {QuickReplySettings}*/settings) { + this.settings = settings; + } + + + + + + + rerender() { + if (!this.dom) return; + const content = this.dom.querySelector('.inline-drawer-content'); + content.innerHTML = ''; + // @ts-ignore + Array.from(this.template.querySelector('.inline-drawer-content').cloneNode(true).children).forEach(el=>{ + content.append(el); + }); + this.prepareDom(); + } + unrender() { + this.dom?.remove(); + this.dom = null; + } + async render() { + if (!this.dom) { + const response = await fetch('/scripts/extensions/quick-reply/html/settings.html', { cache: 'no-store' }); + if (response.ok) { + this.template = document.createRange().createContextualFragment(await response.text()).querySelector('#qr--settings'); + // @ts-ignore + this.dom = this.template.cloneNode(true); + this.prepareDom(); + } else { + warn('failed to fetch settings template'); + } + } + return this.dom; + } + + + prepareGeneralSettings() { + // general settings + this.isEnabled = this.dom.querySelector('#qr--isEnabled'); + this.isEnabled.checked = this.settings.isEnabled; + this.isEnabled.addEventListener('click', ()=>this.onIsEnabled()); + + this.isCombined = this.dom.querySelector('#qr--isCombined'); + this.isCombined.checked = this.settings.isCombined; + this.isCombined.addEventListener('click', ()=>this.onIsCombined()); + } + + /** + * @param {QuickReplyLink} qrl + * @param {Number} idx + */ + renderQrLinkItem(qrl, idx, isGlobal) { + const item = document.createElement('div'); { + item.classList.add('qr--item'); + item.setAttribute('data-order', String(idx)); + const drag = document.createElement('div'); { + drag.classList.add('drag-handle'); + drag.classList.add('ui-sortable-handle'); + drag.textContent = '☰'; + item.append(drag); + } + const set = document.createElement('select'); { + set.addEventListener('change', ()=>{ + qrl.set = QuickReplySet.get(set.value); + this.settings.save(); + }); + QuickReplySet.list.forEach(qrs=>{ + const opt = document.createElement('option'); { + opt.value = qrs.name; + opt.textContent = qrs.name; + opt.selected = qrs == qrl.set; + set.append(opt); + } + }); + item.append(set); + } + const visible = document.createElement('label'); { + visible.classList.add('qr--visible'); + const cb = document.createElement('input'); { + cb.type = 'checkbox'; + cb.checked = qrl.isVisible; + cb.addEventListener('click', ()=>{ + qrl.isVisible = cb.checked; + this.settings.save(); + }); + visible.append(cb); + } + visible.append('Show buttons'); + item.append(visible); + } + const edit = document.createElement('div'); { + edit.classList.add('menu_button'); + edit.classList.add('menu_button_icon'); + edit.classList.add('fa-solid'); + edit.classList.add('fa-pencil'); + edit.title = 'Edit quick reply set'; + edit.addEventListener('click', ()=>{ + this.currentSet.value = qrl.set.name; + this.onQrSetChange(); + }); + item.append(edit); + } + const del = document.createElement('div'); { + del.classList.add('menu_button'); + del.classList.add('menu_button_icon'); + del.classList.add('fa-solid'); + del.classList.add('fa-trash-can'); + del.title = 'Remove quick reply set'; + del.addEventListener('click', ()=>{ + item.remove(); + if (isGlobal) { + this.settings.config.setList.splice(this.settings.config.setList.indexOf(qrl), 1); + this.updateOrder(this.globalSetList); + } else { + this.settings.chatConfig.setList.splice(this.settings.chatConfig.setList.indexOf(qrl), 1); + this.updateOrder(this.chatSetList); + } + this.settings.save(); + }); + item.append(del); + } + } + return item; + } + prepareGlobalSetList() { + // global set list + this.dom.querySelector('#qr--global-setListAdd').addEventListener('click', ()=>{ + const qrl = new QuickReplyLink(); + qrl.set = QuickReplySet.list[0]; + this.settings.config.setList.push(qrl); + this.globalSetList.append(this.renderQrLinkItem(qrl, this.settings.config.setList.length - 1, true)); + this.settings.save(); + }); + this.globalSetList = this.dom.querySelector('#qr--global-setList'); + // @ts-ignore + $(this.globalSetList).sortable({ + delay: getSortableDelay(), + stop: ()=>this.onGlobalSetListSort(), + }); + this.settings.config.setList.forEach((qrl,idx)=>{ + this.globalSetList.append(this.renderQrLinkItem(qrl, idx, true)); + }); + } + prepareChatSetList() { + // chat set list + this.dom.querySelector('#qr--chat-setListAdd').addEventListener('click', ()=>{ + if (!this.settings.chatConfig) { + toastr.warning('No active chat.'); + return; + } + const qrl = new QuickReplyLink(); + qrl.set = QuickReplySet.list[0]; + this.settings.chatConfig.setList.push(qrl); + this.chatSetList.append(this.renderQrLinkItem(qrl, this.settings.chatConfig.setList.length - 1, false)); + this.settings.save(); + }); + + this.chatSetList = this.dom.querySelector('#qr--chat-setList'); + if (!this.settings.chatConfig) { + const info = document.createElement('small'); { + info.textContent = 'No active chat.'; + this.chatSetList.append(info); + } + } + // @ts-ignore + $(this.chatSetList).sortable({ + delay: getSortableDelay(), + stop: ()=>this.onChatSetListSort(), + }); + this.settings.chatConfig?.setList?.forEach((qrl,idx)=>{ + this.chatSetList.append(this.renderQrLinkItem(qrl, idx, false)); + }); + } + + prepareQrEditor() { + // qr editor + this.dom.querySelector('#qr--set-add').addEventListener('click', async()=>{ + this.currentQrSet.addQuickReply(); + }); + this.qrList = this.dom.querySelector('#qr--set-qrList'); + this.currentSet = this.dom.querySelector('#qr--set'); + this.currentSet.addEventListener('change', ()=>this.onQrSetChange()); + QuickReplySet.list.forEach(qrs=>{ + const opt = document.createElement('option'); { + opt.value = qrs.name; + opt.textContent = qrs.name; + this.currentSet.append(opt); + } + }); + this.disableSend = this.dom.querySelector('#qr--disableSend'); + this.disableSend.addEventListener('click', ()=>{ + const qrs = this.currentQrSet; + qrs.disableSend = this.disableSend.checked; + qrs.save(); + }); + this.placeBeforeInput = this.dom.querySelector('#qr--placeBeforeInput'); + this.placeBeforeInput.addEventListener('click', ()=>{ + const qrs = this.currentQrSet; + qrs.placeBeforeInput = this.placeBeforeInput.checked; + qrs.save(); + }); + this.injectInput = this.dom.querySelector('#qr--injectInput'); + this.injectInput.addEventListener('click', ()=>{ + const qrs = this.currentQrSet; + qrs.injectInput = this.injectInput.checked; + qrs.save(); + }); + this.onQrSetChange(); + } + onQrSetChange() { + this.currentQrSet = QuickReplySet.get(this.currentSet.value); + this.disableSend.checked = this.currentQrSet.disableSend; + this.placeBeforeInput.checked = this.currentQrSet.placeBeforeInput; + this.injectInput.checked = this.currentQrSet.injectInput; + this.qrList.innerHTML = ''; + const qrsDom = this.currentQrSet.renderSettings(); + this.qrList.append(qrsDom); + // @ts-ignore + $(qrsDom).sortable({ + delay: getSortableDelay(), + stop: ()=>this.onQrListSort(), + }); + } + + + prepareDom() { + this.prepareGeneralSettings(); + this.prepareGlobalSetList(); + this.prepareChatSetList(); + this.prepareQrEditor(); + } + + + + + async onIsEnabled() { + this.settings.isEnabled = this.isEnabled.checked; + this.settings.save(); + } + + async onIsCombined() { + this.settings.isCombined = this.isCombined.checked; + this.settings.save(); + } + + async onGlobalSetListSort() { + this.settings.config.setList = Array.from(this.globalSetList.children).map((it,idx)=>{ + const set = this.settings.config.setList[Number(it.getAttribute('data-order'))]; + it.setAttribute('data-order', String(idx)); + return set; + }); + this.settings.save(); + } + + async onChatSetListSort() { + this.settings.chatConfig.setList = Array.from(this.chatSetList.children).map((it,idx)=>{ + const set = this.settings.chatConfig.setList[Number(it.getAttribute('data-order'))]; + it.setAttribute('data-order', String(idx)); + return set; + }); + this.settings.save(); + } + + updateOrder(list) { + Array.from(list.children).forEach((it,idx)=>{ + it.setAttribute('data-order', idx); + }); + } + + async onQrListSort() { + this.currentQrSet.qrList = Array.from(this.qrList.querySelectorAll('.qr--set-item')).map((it,idx)=>{ + const qr = this.currentQrSet.qrList.find(qr=>qr.id == Number(it.getAttribute('data-id'))); + it.setAttribute('data-order', String(idx)); + return qr; + }); + this.currentQrSet.save(); + } +} diff --git a/public/scripts/extensions/quick-reply/style.css b/public/scripts/extensions/quick-reply/style.css new file mode 100644 index 000000000..970b2162a --- /dev/null +++ b/public/scripts/extensions/quick-reply/style.css @@ -0,0 +1,114 @@ +#qr--bar { + outline: none; + margin: 0; + transition: 0.3s; + opacity: 0.7; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + max-width: 100%; + overflow-x: auto; + order: 1; + position: relative; +} +#qr--bar > .qr--buttons { + margin: 0; + padding: 0; + display: flex; + justify-content: center; + flex-wrap: wrap; + gap: 5px; + width: 100%; +} +#qr--bar > .qr--buttons > .qr--buttons { + display: contents; +} +#qr--bar > .qr--buttons .qr--button { + color: var(--SmartThemeBodyColor); + background-color: var(--black50a); + border: 1px solid var(--SmartThemeBorderColor); + border-radius: 10px; + padding: 3px 5px; + margin: 3px 0; + cursor: pointer; + transition: 0.3s; + display: flex; + align-items: center; + justify-content: center; + text-align: center; +} +#qr--bar > .qr--buttons .qr--button:hover { + opacity: 1; + filter: brightness(1.2); +} +#qr--settings .qr--head { + display: flex; + align-items: baseline; + gap: 1em; +} +#qr--settings .qr--head > .qr--title { + font-weight: bold; +} +#qr--settings .qr--head > .qr--actions { + display: flex; + flex-direction: row; + align-items: baseline; + gap: 0.5em; +} +#qr--settings .qr--setList > .qr--item { + display: flex; + flex-direction: row; + gap: 0.5em; + align-items: baseline; + padding: 0 0.5em; +} +#qr--settings .qr--setList > .qr--item > .qr--visible { + flex: 1 1 200px; + display: flex; + flex-direction: row; +} +#qr--settings #qr--set-settings #qr--injectInputContainer { + flex-wrap: nowrap; +} +#qr--settings #qr--set-qrList .qr--set-qrListContents { + padding: 0 0.5em; +} +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item { + display: flex; + flex-direction: row; + gap: 0.5em; + align-items: baseline; + padding: 0.25em 0; +} +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(1) { + flex: 0 0 auto; +} +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(2) { + flex: 1 1 25%; +} +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(3) { + flex: 0 0 auto; +} +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(4) { + flex: 0 0 auto; +} +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(5) { + flex: 1 1 75%; +} +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(6) { + flex: 0 0 auto; +} +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemLabel, +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--action { + margin: 0; +} +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemMessage { + font-size: smaller; +} +#qr--qrOptions > #qr--ctxEditor .qr--ctxItem { + display: flex; + flex-direction: row; + gap: 0.5em; + align-items: baseline; +} diff --git a/public/scripts/extensions/quick-reply/style.less b/public/scripts/extensions/quick-reply/style.less new file mode 100644 index 000000000..9a7f14916 --- /dev/null +++ b/public/scripts/extensions/quick-reply/style.less @@ -0,0 +1,124 @@ +#qr--bar { + outline: none; + margin: 0; + transition: 0.3s; + opacity: 0.7; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + max-width: 100%; + overflow-x: auto; + order: 1; + position: relative; + + > .qr--buttons { + margin: 0; + padding: 0; + display: flex; + justify-content: center; + flex-wrap: wrap; + gap: 5px; + width: 100%; + + > .qr--buttons { + display: contents; + } + + .qr--button { + color: var(--SmartThemeBodyColor); + background-color: var(--black50a); + border: 1px solid var(--SmartThemeBorderColor); + border-radius: 10px; + padding: 3px 5px; + margin: 3px 0; + cursor: pointer; + transition: 0.3s; + display: flex; + align-items: center; + justify-content: center; + text-align: center; + &:hover { + opacity: 1; + filter: brightness(1.2); + } + } + } +} + + + +#qr--settings { + .qr--head { + display: flex; + align-items: baseline; + gap: 1em; + > .qr--title { + font-weight: bold; + } + > .qr--actions { + display: flex; + flex-direction: row; + align-items: baseline; + gap: 0.5em; + } + } + .qr--setList { + > .qr--item { + display: flex; + flex-direction: row; + gap: 0.5em; + align-items: baseline; + padding: 0 0.5em; + > .qr--visible { + flex: 1 1 200px; + display: flex; + flex-direction: row; + } + } + } + #qr--set-settings { + #qr--injectInputContainer { + flex-wrap: nowrap; + } + } + #qr--set-qrList { + .qr--set-qrListContents > { + padding: 0 0.5em; + > .qr--set-item { + display: flex; + flex-direction: row; + gap: 0.5em; + align-items: baseline; + padding: 0.25em 0; + > :nth-child(1) { flex: 0 0 auto; } + > :nth-child(2) { flex: 1 1 25%; } + > :nth-child(3) { flex: 0 0 auto; } + > :nth-child(4) { flex: 0 0 auto; } + > :nth-child(5) { flex: 1 1 75%; } + > :nth-child(6) { flex: 0 0 auto; } + .qr--set-itemLabel, .qr--action { + margin: 0; + } + .qr--set-itemMessage { + font-size: smaller; + } + } + } + } +} + + + + + +#qr--qrOptions { + > #qr--ctxEditor { + .qr--ctxItem { + display: flex; + flex-direction: row; + gap: 0.5em; + align-items: baseline; + } + } +} From 34decf1c05d4f9505ee8c61134ae0f5facbbe78d Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Wed, 20 Dec 2023 14:04:28 +0000 Subject: [PATCH 003/522] add creating of new QR sets --- .../scripts/extensions/quick-reply/index.js | 1 - .../quick-reply/src/ui/SettingsUi.js | 33 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/public/scripts/extensions/quick-reply/index.js b/public/scripts/extensions/quick-reply/index.js index 89eee9abf..0b441e105 100644 --- a/public/scripts/extensions/quick-reply/index.js +++ b/public/scripts/extensions/quick-reply/index.js @@ -15,7 +15,6 @@ import { SettingsUi } from './src/ui/SettingsUi.js'; //TODO context menus //TODO move advanced QR options into own UI class //TODO slash commands -//TODO create new QR set //TODO delete QR set //TODO easy way to CRUD QRs and sets //TODO easy way to set global and chat sets diff --git a/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js b/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js index dae46345f..5dc201861 100644 --- a/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js +++ b/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js @@ -1,3 +1,4 @@ +import { callPopup } from '../../../../../script.js'; import { getSortableDelay } from '../../../../utils.js'; import { warn } from '../../index.js'; import { QuickReplyLink } from '../QuickReplyLink.js'; @@ -206,6 +207,7 @@ export class SettingsUi { prepareQrEditor() { // qr editor + this.dom.querySelector('#qr--set-new').addEventListener('click', async()=>this.addQrSet()); this.dom.querySelector('#qr--set-add').addEventListener('click', async()=>{ this.currentQrSet.addQuickReply(); }); @@ -307,4 +309,35 @@ export class SettingsUi { }); this.currentQrSet.save(); } + + async addQrSet() { + const name = await callPopup('Quick Reply Set Name:', 'input'); + if (name && name.length > 0) { + const oldQrs = QuickReplySet.get(name); + if (oldQrs) { + const replace = await callPopup(`A Quick Reply Set named "${name} already exists.\nDo you want to overwrite the existing Quick Reply Set?\nThe existing set will be deleted. This cannot be undone.`, 'confirm'); + if (replace) { + const qrs = new QuickReplySet(); + qrs.name = name; + qrs.addQuickReply(); + QuickReplySet.list[QuickReplySet.list.indexOf(oldQrs)] = qrs; + this.currentSet.value = name; + this.onQrSetChange(); + } + } else { + const qrs = new QuickReplySet(); + qrs.name = name; + qrs.addQuickReply(); + const idx = QuickReplySet.list.findIndex(it=>it.name.localeCompare(name) == 1); + QuickReplySet.list.splice(idx, 0, qrs); + const opt = document.createElement('option'); { + opt.value = qrs.name; + opt.textContent = qrs.name; + this.currentSet.children[idx].insertAdjacentElement('beforebegin', opt); + } + this.currentSet.value = name; + this.onQrSetChange(); + } + } + } } From 2648b3c801438721c7249cc3eb6a95d87432a7cb Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Wed, 20 Dec 2023 15:49:27 +0000 Subject: [PATCH 004/522] restructuring --- .../extensions/quick-reply/html/settings.html | 67 ++++---- .../scripts/extensions/quick-reply/index.js | 1 + .../quick-reply/src/QuickReplyConfig.js | 87 +++++++++- .../quick-reply/src/QuickReplyContextLink.js | 3 - .../quick-reply/src/QuickReplyLink.js | 24 --- .../quick-reply/src/QuickReplySet.js | 16 ++ .../quick-reply/src/QuickReplySetLink.js | 126 ++++++++++++++ .../quick-reply/src/QuickReplySettings.js | 44 ++++- .../quick-reply/src/ui/SettingsUi.js | 154 ++++-------------- 9 files changed, 338 insertions(+), 184 deletions(-) delete mode 100644 public/scripts/extensions/quick-reply/src/QuickReplyLink.js create mode 100644 public/scripts/extensions/quick-reply/src/QuickReplySetLink.js diff --git a/public/scripts/extensions/quick-reply/html/settings.html b/public/scripts/extensions/quick-reply/html/settings.html index 4b5d0d8b3..09cae2872 100644 --- a/public/scripts/extensions/quick-reply/html/settings.html +++ b/public/scripts/extensions/quick-reply/html/settings.html @@ -14,47 +14,54 @@
-
-
Global Quick Reply Sets
-
- +
+
+
Global Quick Reply Sets
+
+ +
+
-

-
-
Chat Quick Reply Sets
-
- +
+
+
Chat Quick Reply Sets
+
+ +
+
-

-
-
Edit Quick Replies
-
- - +
+
+
Edit Quick Replies
+
+ + + +
+
+
+ + + +
+
+
+
-
-
- - - -
-
-
-
diff --git a/public/scripts/extensions/quick-reply/index.js b/public/scripts/extensions/quick-reply/index.js index 0b441e105..fa82b88b1 100644 --- a/public/scripts/extensions/quick-reply/index.js +++ b/public/scripts/extensions/quick-reply/index.js @@ -16,6 +16,7 @@ import { SettingsUi } from './src/ui/SettingsUi.js'; //TODO move advanced QR options into own UI class //TODO slash commands //TODO delete QR set +//TODO handle replacing QR set (create->overwrite): update configs that use this set //TODO easy way to CRUD QRs and sets //TODO easy way to set global and chat sets diff --git a/public/scripts/extensions/quick-reply/src/QuickReplyConfig.js b/public/scripts/extensions/quick-reply/src/QuickReplyConfig.js index 701d00be6..7afbe69b2 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReplyConfig.js +++ b/public/scripts/extensions/quick-reply/src/QuickReplyConfig.js @@ -1,19 +1,98 @@ -import { QuickReplyLink } from './QuickReplyLink.js'; +import { getSortableDelay } from '../../../utils.js'; +import { QuickReplySetLink } from './QuickReplySetLink.js'; +import { QuickReplySet } from './QuickReplySet.js'; export class QuickReplyConfig { - /**@type {QuickReplyLink[]}*/ setList = []; + /**@type {QuickReplySetLink[]}*/ setList = []; + /**@type {Boolean}*/ isGlobal; + + /**@type {Function}*/ onUpdate; + /**@type {Function}*/ onRequestEditSet; + + /**@type {HTMLElement}*/ dom; + /**@type {HTMLElement}*/ setListDom; static from(props) { - props.setList = props.setList?.map(it=>QuickReplyLink.from(it)) ?? []; - return Object.assign(new this(), props); + props.setList = props.setList?.map(it=>QuickReplySetLink.from(it)) ?? []; + const instance = Object.assign(new this(), props); + instance.init(); + return instance; } + init() { + this.setList.forEach(it=>this.hookQuickReplyLink(it)); + } + + + + + renderSettingsInto(/**@type {HTMLElement}*/root) { + /**@type {HTMLElement}*/ + const setList = root.querySelector('.qr--setList'); + this.setListDom = setList; + setList.innerHTML = ''; + root.querySelector('.qr--setListAdd').addEventListener('click', ()=>{ + const qrl = new QuickReplySetLink(); + qrl.set = QuickReplySet.list[0]; + this.hookQuickReplyLink(qrl); + this.setList.push(qrl); + setList.append(qrl.renderSettings(this.setList.length - 1)); + this.update(); + }); + // @ts-ignore + $(setList).sortable({ + delay: getSortableDelay(), + stop: ()=>this.onSetListSort(), + }); + this.setList.forEach((qrl,idx)=>setList.append(qrl.renderSettings(idx))); + } + + + onSetListSort() { + this.setList = Array.from(this.setListDom.children).map((it,idx)=>{ + const qrl = this.setList[Number(it.getAttribute('data-order'))]; + qrl.index = idx; + it.setAttribute('data-order', String(idx)); + return qrl; + }); + this.update(); + } + + + + + /** + * @param {QuickReplySetLink} qrl + */ + hookQuickReplyLink(qrl) { + qrl.onDelete = ()=>this.deleteQuickReplyLink(qrl); + qrl.onUpdate = ()=>this.update(); + qrl.onRequestEditSet = ()=>this.requestEditSet(qrl.set); + } + + deleteQuickReplyLink(qrl) { + this.setList.splice(this.setList.indexOf(qrl), 1); + this.update(); + } + + update() { + if (this.onUpdate) { + this.onUpdate(this); + } + } + + requestEditSet(qrs) { + if (this.onRequestEditSet) { + this.onRequestEditSet(qrs); + } + } + toJSON() { return { setList: this.setList, diff --git a/public/scripts/extensions/quick-reply/src/QuickReplyContextLink.js b/public/scripts/extensions/quick-reply/src/QuickReplyContextLink.js index 2c1d1e51f..a189bba8d 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReplyContextLink.js +++ b/public/scripts/extensions/quick-reply/src/QuickReplyContextLink.js @@ -13,9 +13,6 @@ export class QuickReplyContextLink { /**@type {QuickReplySet}*/ set; /**@type {Boolean}*/ isChained = false; - - - toJSON() { return { set: this.set?.name, diff --git a/public/scripts/extensions/quick-reply/src/QuickReplyLink.js b/public/scripts/extensions/quick-reply/src/QuickReplyLink.js deleted file mode 100644 index 05d37f1b4..000000000 --- a/public/scripts/extensions/quick-reply/src/QuickReplyLink.js +++ /dev/null @@ -1,24 +0,0 @@ -import { QuickReplySet } from './QuickReplySet.js'; - -export class QuickReplyLink { - /**@type {QuickReplySet}*/ set; - /**@type {Boolean}*/ isVisible = true; - - - - - static from(props) { - props.set = QuickReplySet.get(props.set); - return Object.assign(new this(), props); - } - - - - - toJSON() { - return { - set: this.set.name, - isVisible: this.isVisible, - }; - } -} diff --git a/public/scripts/extensions/quick-reply/src/QuickReplySet.js b/public/scripts/extensions/quick-reply/src/QuickReplySet.js index 5fb534f0f..a66b3455b 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReplySet.js +++ b/public/scripts/extensions/quick-reply/src/QuickReplySet.js @@ -189,4 +189,20 @@ export class QuickReplySet { warn(`Failed to save Quick Reply Set: ${this.name}`); } } + + async delete() { + const response = await fetch('/deletequickreply', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify(this), + }); + + if (response.ok) { + this.unrender(); + const idx = QuickReplySet.list.indexOf(this); + QuickReplySet.list.splice(idx, 1); + } else { + warn(`Failed to delete Quick Reply Set: ${this.name}`); + } + } } diff --git a/public/scripts/extensions/quick-reply/src/QuickReplySetLink.js b/public/scripts/extensions/quick-reply/src/QuickReplySetLink.js new file mode 100644 index 000000000..fb32870ac --- /dev/null +++ b/public/scripts/extensions/quick-reply/src/QuickReplySetLink.js @@ -0,0 +1,126 @@ +import { QuickReplySet } from './QuickReplySet.js'; + +export class QuickReplySetLink { + static from(props) { + props.set = QuickReplySet.get(props.set); + return Object.assign(new this(), props); + } + + + + + /**@type {QuickReplySet}*/ set; + /**@type {Boolean}*/ isVisible = true; + + /**@type {Number}*/ index; + + /**@type {Function}*/ onUpdate; + /**@type {Function}*/ onRequestEditSet; + /**@type {Function}*/ onDelete; + + /**@type {HTMLElement}*/ settingsDom; + + + + + renderSettings(idx) { + if (!this.settingsDom || idx != this.index) { + this.index = idx; + const item = document.createElement('div'); { + this.settingsDom = item; + item.classList.add('qr--item'); + item.setAttribute('data-order', String(this.index)); + const drag = document.createElement('div'); { + drag.classList.add('drag-handle'); + drag.classList.add('ui-sortable-handle'); + drag.textContent = '☰'; + item.append(drag); + } + const set = document.createElement('select'); { + set.classList.add('qr--set'); + set.addEventListener('change', ()=>{ + this.set = QuickReplySet.get(set.value); + this.update(); + }); + QuickReplySet.list.forEach(qrs=>{ + const opt = document.createElement('option'); { + opt.value = qrs.name; + opt.textContent = qrs.name; + opt.selected = qrs == this.set; + set.append(opt); + } + }); + item.append(set); + } + const visible = document.createElement('label'); { + visible.classList.add('qr--visible'); + const cb = document.createElement('input'); { + cb.type = 'checkbox'; + cb.checked = this.isVisible; + cb.addEventListener('click', ()=>{ + this.isVisible = cb.checked; + this.update(); + }); + visible.append(cb); + } + visible.append('Show buttons'); + item.append(visible); + } + const edit = document.createElement('div'); { + edit.classList.add('menu_button'); + edit.classList.add('menu_button_icon'); + edit.classList.add('fa-solid'); + edit.classList.add('fa-pencil'); + edit.title = 'Edit quick reply set'; + edit.addEventListener('click', ()=>this.requestEditSet()); + item.append(edit); + } + const del = document.createElement('div'); { + del.classList.add('qr--del'); + del.classList.add('menu_button'); + del.classList.add('menu_button_icon'); + del.classList.add('fa-solid'); + del.classList.add('fa-trash-can'); + del.title = 'Remove quick reply set'; + del.addEventListener('click', ()=>this.delete()); + item.append(del); + } + } + } + return this.settingsDom; + } + unrenderSettings() { + this.settingsDom?.remove(); + this.settingsDom = null; + } + + + + + update() { + if (this.onUpdate) { + this.onUpdate(this); + } + } + requestEditSet() { + if (this.onRequestEditSet) { + this.onRequestEditSet(this.set); + } + } + delete() { + this.unrenderSettings(); + if (this.onDelete) { + this.onDelete(); + } + } + + + + + toJSON() { + return { + set: this.set.name, + isVisible: this.isVisible, + }; + } +} diff --git a/public/scripts/extensions/quick-reply/src/QuickReplySettings.js b/public/scripts/extensions/quick-reply/src/QuickReplySettings.js index c2052e911..8c6cba420 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReplySettings.js +++ b/public/scripts/extensions/quick-reply/src/QuickReplySettings.js @@ -5,7 +5,9 @@ import { QuickReplyConfig } from './QuickReplyConfig.js'; export class QuickReplySettings { static from(props) { props.config = QuickReplyConfig.from(props.config); - return Object.assign(new this(), props); + const instance = Object.assign(new this(), props); + instance.init(); + return instance; } @@ -14,9 +16,41 @@ export class QuickReplySettings { /**@type {Boolean}*/ isEnabled = false; /**@type {Boolean}*/ isCombined = false; /**@type {QuickReplyConfig}*/ config; - /**@type {QuickReplyConfig}*/ chatConfig; + /**@type {QuickReplyConfig}*/ _chatConfig; + get chatConfig() { + return this._chatConfig; + } + set chatConfig(value) { + if (this._chatConfig != value) { + this.unhookConfig(this._chatConfig); + this._chatConfig = value; + this.hookConfig(this._chatConfig); + } + } /**@type {Function}*/ onSave; + /**@type {Function}*/ onRequestEditSet; + + + + + init() { + this.hookConfig(this.config); + this.hookConfig(this.chatConfig); + } + + hookConfig(config) { + if (config) { + config.onUpdate = ()=>this.save(); + config.onRequestEditSet = (qrs)=>this.requestEditSet(qrs); + } + } + unhookConfig(config) { + if (config) { + config.onUpdate = null; + config.onRequestEditSet = null; + } + } @@ -33,6 +67,12 @@ export class QuickReplySettings { } } + requestEditSet(qrs) { + if (this.onRequestEditSet) { + this.onRequestEditSet(qrs); + } + } + toJSON() { return { isEnabled: this.isEnabled, diff --git a/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js b/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js index 5dc201861..eb7b24b5c 100644 --- a/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js +++ b/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js @@ -1,7 +1,6 @@ import { callPopup } from '../../../../../script.js'; import { getSortableDelay } from '../../../../utils.js'; import { warn } from '../../index.js'; -import { QuickReplyLink } from '../QuickReplyLink.js'; import { QuickReplySet } from '../QuickReplySet.js'; // eslint-disable-next-line no-unused-vars import { QuickReplySettings } from '../QuickReplySettings.js'; @@ -30,6 +29,7 @@ export class SettingsUi { constructor(/**@type {QuickReplySettings}*/settings) { this.settings = settings; + settings.onRequestEditSet = (qrs) => this.selectQrSet(qrs); } @@ -78,135 +78,23 @@ export class SettingsUi { this.isCombined.addEventListener('click', ()=>this.onIsCombined()); } - /** - * @param {QuickReplyLink} qrl - * @param {Number} idx - */ - renderQrLinkItem(qrl, idx, isGlobal) { - const item = document.createElement('div'); { - item.classList.add('qr--item'); - item.setAttribute('data-order', String(idx)); - const drag = document.createElement('div'); { - drag.classList.add('drag-handle'); - drag.classList.add('ui-sortable-handle'); - drag.textContent = '☰'; - item.append(drag); - } - const set = document.createElement('select'); { - set.addEventListener('change', ()=>{ - qrl.set = QuickReplySet.get(set.value); - this.settings.save(); - }); - QuickReplySet.list.forEach(qrs=>{ - const opt = document.createElement('option'); { - opt.value = qrs.name; - opt.textContent = qrs.name; - opt.selected = qrs == qrl.set; - set.append(opt); - } - }); - item.append(set); - } - const visible = document.createElement('label'); { - visible.classList.add('qr--visible'); - const cb = document.createElement('input'); { - cb.type = 'checkbox'; - cb.checked = qrl.isVisible; - cb.addEventListener('click', ()=>{ - qrl.isVisible = cb.checked; - this.settings.save(); - }); - visible.append(cb); - } - visible.append('Show buttons'); - item.append(visible); - } - const edit = document.createElement('div'); { - edit.classList.add('menu_button'); - edit.classList.add('menu_button_icon'); - edit.classList.add('fa-solid'); - edit.classList.add('fa-pencil'); - edit.title = 'Edit quick reply set'; - edit.addEventListener('click', ()=>{ - this.currentSet.value = qrl.set.name; - this.onQrSetChange(); - }); - item.append(edit); - } - const del = document.createElement('div'); { - del.classList.add('menu_button'); - del.classList.add('menu_button_icon'); - del.classList.add('fa-solid'); - del.classList.add('fa-trash-can'); - del.title = 'Remove quick reply set'; - del.addEventListener('click', ()=>{ - item.remove(); - if (isGlobal) { - this.settings.config.setList.splice(this.settings.config.setList.indexOf(qrl), 1); - this.updateOrder(this.globalSetList); - } else { - this.settings.chatConfig.setList.splice(this.settings.chatConfig.setList.indexOf(qrl), 1); - this.updateOrder(this.chatSetList); - } - this.settings.save(); - }); - item.append(del); - } - } - return item; - } prepareGlobalSetList() { - // global set list - this.dom.querySelector('#qr--global-setListAdd').addEventListener('click', ()=>{ - const qrl = new QuickReplyLink(); - qrl.set = QuickReplySet.list[0]; - this.settings.config.setList.push(qrl); - this.globalSetList.append(this.renderQrLinkItem(qrl, this.settings.config.setList.length - 1, true)); - this.settings.save(); - }); - this.globalSetList = this.dom.querySelector('#qr--global-setList'); - // @ts-ignore - $(this.globalSetList).sortable({ - delay: getSortableDelay(), - stop: ()=>this.onGlobalSetListSort(), - }); - this.settings.config.setList.forEach((qrl,idx)=>{ - this.globalSetList.append(this.renderQrLinkItem(qrl, idx, true)); - }); + this.settings.config.renderSettingsInto(this.dom.querySelector('#qr--global')); } prepareChatSetList() { - // chat set list - this.dom.querySelector('#qr--chat-setListAdd').addEventListener('click', ()=>{ - if (!this.settings.chatConfig) { - toastr.warning('No active chat.'); - return; - } - const qrl = new QuickReplyLink(); - qrl.set = QuickReplySet.list[0]; - this.settings.chatConfig.setList.push(qrl); - this.chatSetList.append(this.renderQrLinkItem(qrl, this.settings.chatConfig.setList.length - 1, false)); - this.settings.save(); - }); - - this.chatSetList = this.dom.querySelector('#qr--chat-setList'); - if (!this.settings.chatConfig) { - const info = document.createElement('small'); { + if (this.settings.chatConfig) { + this.settings.chatConfig.renderSettingsInto(this.dom.querySelector('#qr--chat')); + } else { + const info = document.createElement('div'); { info.textContent = 'No active chat.'; - this.chatSetList.append(info); } + this.dom.querySelector('#qr--chat').append(info); } - // @ts-ignore - $(this.chatSetList).sortable({ - delay: getSortableDelay(), - stop: ()=>this.onChatSetListSort(), - }); - this.settings.chatConfig?.setList?.forEach((qrl,idx)=>{ - this.chatSetList.append(this.renderQrLinkItem(qrl, idx, false)); - }); } prepareQrEditor() { // qr editor + this.dom.querySelector('#qr--set-delete').addEventListener('click', async()=>this.deleteQrSet()); this.dom.querySelector('#qr--set-new').addEventListener('click', async()=>this.addQrSet()); this.dom.querySelector('#qr--set-add').addEventListener('click', async()=>{ this.currentQrSet.addQuickReply(); @@ -310,12 +198,31 @@ export class SettingsUi { this.currentQrSet.save(); } + async deleteQrSet() { + const confirmed = await callPopup(`Are you sure you want to delete the Quick Reply Set "${this.currentQrSet.name}"? This cannot be undone.`, 'confirm'); + if (confirmed) { + const idx = QuickReplySet.list.indexOf(this.currentQrSet); + await this.currentQrSet.delete(); + this.currentSet.children[idx].remove(); + [ + ...Array.from(this.globalSetList.querySelectorAll('.qr--item > .qr--set')), + ...Array.from(this.chatSetList.querySelectorAll('.qr--item > .qr--set')), + ] + // @ts-ignore + .filter(it=>it.value == this.currentQrSet.name) + // @ts-ignore + .forEach(it=>it.closest('.qr--item').querySelector('.qr--del').click()) + ; + this.onQrSetChange(); + } + } + async addQrSet() { const name = await callPopup('Quick Reply Set Name:', 'input'); if (name && name.length > 0) { const oldQrs = QuickReplySet.get(name); if (oldQrs) { - const replace = await callPopup(`A Quick Reply Set named "${name} already exists.\nDo you want to overwrite the existing Quick Reply Set?\nThe existing set will be deleted. This cannot be undone.`, 'confirm'); + const replace = await callPopup(`A Quick Reply Set named "${name}" already exists.\nDo you want to overwrite the existing Quick Reply Set?\nThe existing set will be deleted. This cannot be undone.`, 'confirm'); if (replace) { const qrs = new QuickReplySet(); qrs.name = name; @@ -340,4 +247,9 @@ export class SettingsUi { } } } + + selectQrSet(qrs) { + this.currentSet.value = qrs.name; + this.onQrSetChange(); + } } From 41a88e165ce293f1d263c2827d56fd6052228453 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Wed, 20 Dec 2023 15:50:19 +0000 Subject: [PATCH 005/522] add deletequickreply request handler --- server.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/server.js b/server.js index 153cb908f..6124fb1cd 100644 --- a/server.js +++ b/server.js @@ -294,6 +294,19 @@ app.post('/savequickreply', jsonParser, (request, response) => { return response.sendStatus(200); }); +app.post('/deletequickreply', jsonParser, (request, response) => { + if (!request.body || !request.body.name) { + return response.sendStatus(400); + } + + const filename = path.join(DIRECTORIES.quickreplies, sanitize(request.body.name) + '.json'); + if (fs.existsSync(filename)) { + fs.unlinkSync(filename); + } + + return response.sendStatus(200); +}); + app.post('/uploaduseravatar', urlencodedParser, async (request, response) => { if (!request.file) return response.sendStatus(400); From ac09fa6019e4f8c8367ddc203d6974e0e1c40688 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Wed, 20 Dec 2023 17:56:08 +0000 Subject: [PATCH 006/522] handle deleting --- .../scripts/extensions/quick-reply/index.js | 3 +- .../quick-reply/src/QuickReplyConfig.js | 4 +- .../quick-reply/src/QuickReplySet.js | 6 +- .../quick-reply/src/QuickReplySetLink.js | 121 +++++++++--------- .../quick-reply/src/ui/SettingsUi.js | 44 ++++--- 5 files changed, 96 insertions(+), 82 deletions(-) diff --git a/public/scripts/extensions/quick-reply/index.js b/public/scripts/extensions/quick-reply/index.js index fa82b88b1..08ff88949 100644 --- a/public/scripts/extensions/quick-reply/index.js +++ b/public/scripts/extensions/quick-reply/index.js @@ -102,7 +102,8 @@ const loadSettings = async () => { } try { settings = QuickReplySettings.from(extension_settings.quickReplyV2); - } catch { + } catch (ex) { + debugger; settings = QuickReplySettings.from(defaultSettings); } }; diff --git a/public/scripts/extensions/quick-reply/src/QuickReplyConfig.js b/public/scripts/extensions/quick-reply/src/QuickReplyConfig.js index 7afbe69b2..c7ca95c5a 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReplyConfig.js +++ b/public/scripts/extensions/quick-reply/src/QuickReplyConfig.js @@ -16,7 +16,7 @@ export class QuickReplyConfig { static from(props) { - props.setList = props.setList?.map(it=>QuickReplySetLink.from(it)) ?? []; + props.setList = props.setList?.map(it=>QuickReplySetLink.from(it))?.filter(it=>it.set) ?? []; const instance = Object.assign(new this(), props); instance.init(); return instance; @@ -50,7 +50,7 @@ export class QuickReplyConfig { delay: getSortableDelay(), stop: ()=>this.onSetListSort(), }); - this.setList.forEach((qrl,idx)=>setList.append(qrl.renderSettings(idx))); + this.setList.filter(it=>!it.set.isDeleted).forEach((qrl,idx)=>setList.append(qrl.renderSettings(idx))); } diff --git a/public/scripts/extensions/quick-reply/src/QuickReplySet.js b/public/scripts/extensions/quick-reply/src/QuickReplySet.js index a66b3455b..61156750e 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReplySet.js +++ b/public/scripts/extensions/quick-reply/src/QuickReplySet.js @@ -26,17 +26,16 @@ export class QuickReplySet { /**@type {String}*/ name; - /**@type {Boolean}*/ disableSend = false; /**@type {Boolean}*/ placeBeforeInput = false; /**@type {Boolean}*/ injectInput = false; - /**@type {QuickReply[]}*/ qrList = []; /**@type {Number}*/ idIndex = 0; - /**@type {Function}*/ save; + /**@type {Boolean}*/ isDeleted = false; + /**@type {Function}*/ save; /**@type {HTMLElement}*/ dom; /**@type {HTMLElement}*/ settingsDom; @@ -201,6 +200,7 @@ export class QuickReplySet { this.unrender(); const idx = QuickReplySet.list.indexOf(this); QuickReplySet.list.splice(idx, 1); + this.isDeleted = true; } else { warn(`Failed to delete Quick Reply Set: ${this.name}`); } diff --git a/public/scripts/extensions/quick-reply/src/QuickReplySetLink.js b/public/scripts/extensions/quick-reply/src/QuickReplySetLink.js index fb32870ac..0c8ec2994 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReplySetLink.js +++ b/public/scripts/extensions/quick-reply/src/QuickReplySetLink.js @@ -1,9 +1,14 @@ import { QuickReplySet } from './QuickReplySet.js'; + + + export class QuickReplySetLink { static from(props) { props.set = QuickReplySet.get(props.set); - return Object.assign(new this(), props); + /**@type {QuickReplySetLink}*/ + const instance = Object.assign(new this(), props); + return instance; } @@ -24,67 +29,65 @@ export class QuickReplySetLink { renderSettings(idx) { - if (!this.settingsDom || idx != this.index) { - this.index = idx; - const item = document.createElement('div'); { - this.settingsDom = item; - item.classList.add('qr--item'); - item.setAttribute('data-order', String(this.index)); - const drag = document.createElement('div'); { - drag.classList.add('drag-handle'); - drag.classList.add('ui-sortable-handle'); - drag.textContent = '☰'; - item.append(drag); - } - const set = document.createElement('select'); { - set.classList.add('qr--set'); - set.addEventListener('change', ()=>{ - this.set = QuickReplySet.get(set.value); + this.index = idx; + const item = document.createElement('div'); { + this.settingsDom = item; + item.classList.add('qr--item'); + item.setAttribute('data-order', String(this.index)); + const drag = document.createElement('div'); { + drag.classList.add('drag-handle'); + drag.classList.add('ui-sortable-handle'); + drag.textContent = '☰'; + item.append(drag); + } + const set = document.createElement('select'); { + set.classList.add('qr--set'); + set.addEventListener('change', ()=>{ + this.set = QuickReplySet.get(set.value); + this.update(); + }); + QuickReplySet.list.forEach(qrs=>{ + const opt = document.createElement('option'); { + opt.value = qrs.name; + opt.textContent = qrs.name; + opt.selected = qrs == this.set; + set.append(opt); + } + }); + item.append(set); + } + const visible = document.createElement('label'); { + visible.classList.add('qr--visible'); + const cb = document.createElement('input'); { + cb.type = 'checkbox'; + cb.checked = this.isVisible; + cb.addEventListener('click', ()=>{ + this.isVisible = cb.checked; this.update(); }); - QuickReplySet.list.forEach(qrs=>{ - const opt = document.createElement('option'); { - opt.value = qrs.name; - opt.textContent = qrs.name; - opt.selected = qrs == this.set; - set.append(opt); - } - }); - item.append(set); - } - const visible = document.createElement('label'); { - visible.classList.add('qr--visible'); - const cb = document.createElement('input'); { - cb.type = 'checkbox'; - cb.checked = this.isVisible; - cb.addEventListener('click', ()=>{ - this.isVisible = cb.checked; - this.update(); - }); - visible.append(cb); - } - visible.append('Show buttons'); - item.append(visible); - } - const edit = document.createElement('div'); { - edit.classList.add('menu_button'); - edit.classList.add('menu_button_icon'); - edit.classList.add('fa-solid'); - edit.classList.add('fa-pencil'); - edit.title = 'Edit quick reply set'; - edit.addEventListener('click', ()=>this.requestEditSet()); - item.append(edit); - } - const del = document.createElement('div'); { - del.classList.add('qr--del'); - del.classList.add('menu_button'); - del.classList.add('menu_button_icon'); - del.classList.add('fa-solid'); - del.classList.add('fa-trash-can'); - del.title = 'Remove quick reply set'; - del.addEventListener('click', ()=>this.delete()); - item.append(del); + visible.append(cb); } + visible.append('Show buttons'); + item.append(visible); + } + const edit = document.createElement('div'); { + edit.classList.add('menu_button'); + edit.classList.add('menu_button_icon'); + edit.classList.add('fa-solid'); + edit.classList.add('fa-pencil'); + edit.title = 'Edit quick reply set'; + edit.addEventListener('click', ()=>this.requestEditSet()); + item.append(edit); + } + const del = document.createElement('div'); { + del.classList.add('qr--del'); + del.classList.add('menu_button'); + del.classList.add('menu_button_icon'); + del.classList.add('fa-solid'); + del.classList.add('fa-trash-can'); + del.title = 'Remove quick reply set'; + del.addEventListener('click', ()=>this.delete()); + item.append(del); } } return this.settingsDom; diff --git a/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js b/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js index eb7b24b5c..1a31a6762 100644 --- a/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js +++ b/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js @@ -79,17 +79,26 @@ export class SettingsUi { } prepareGlobalSetList() { - this.settings.config.renderSettingsInto(this.dom.querySelector('#qr--global')); + const dom = this.template.querySelector('#qr--global'); + const clone = dom.cloneNode(true); + // @ts-ignore + this.settings.config.renderSettingsInto(clone); + this.dom.querySelector('#qr--global').replaceWith(clone); } prepareChatSetList() { + const dom = this.template.querySelector('#qr--chat'); + const clone = dom.cloneNode(true); if (this.settings.chatConfig) { - this.settings.chatConfig.renderSettingsInto(this.dom.querySelector('#qr--chat')); + // @ts-ignore + this.settings.chatConfig.renderSettingsInto(clone); } else { const info = document.createElement('div'); { info.textContent = 'No active chat.'; + // @ts-ignore + clone.append(info); } - this.dom.querySelector('#qr--chat').append(info); } + this.dom.querySelector('#qr--chat').replaceWith(clone); } prepareQrEditor() { @@ -201,19 +210,8 @@ export class SettingsUi { async deleteQrSet() { const confirmed = await callPopup(`Are you sure you want to delete the Quick Reply Set "${this.currentQrSet.name}"? This cannot be undone.`, 'confirm'); if (confirmed) { - const idx = QuickReplySet.list.indexOf(this.currentQrSet); await this.currentQrSet.delete(); - this.currentSet.children[idx].remove(); - [ - ...Array.from(this.globalSetList.querySelectorAll('.qr--item > .qr--set')), - ...Array.from(this.chatSetList.querySelectorAll('.qr--item > .qr--set')), - ] - // @ts-ignore - .filter(it=>it.value == this.currentQrSet.name) - // @ts-ignore - .forEach(it=>it.closest('.qr--item').querySelector('.qr--del').click()) - ; - this.onQrSetChange(); + this.rerender(); } } @@ -230,20 +228,32 @@ export class SettingsUi { QuickReplySet.list[QuickReplySet.list.indexOf(oldQrs)] = qrs; this.currentSet.value = name; this.onQrSetChange(); + this.prepareGlobalSetList(); + this.prepareChatSetList(); } } else { const qrs = new QuickReplySet(); qrs.name = name; qrs.addQuickReply(); const idx = QuickReplySet.list.findIndex(it=>it.name.localeCompare(name) == 1); - QuickReplySet.list.splice(idx, 0, qrs); + if (idx > -1) { + QuickReplySet.list.splice(idx, 0, qrs); + } else { + QuickReplySet.list.push(qrs); + } const opt = document.createElement('option'); { opt.value = qrs.name; opt.textContent = qrs.name; - this.currentSet.children[idx].insertAdjacentElement('beforebegin', opt); + if (idx > -1) { + this.currentSet.children[idx].insertAdjacentElement('beforebegin', opt); + } else { + this.currentSet.append(opt); + } } this.currentSet.value = name; this.onQrSetChange(); + this.prepareGlobalSetList(); + this.prepareChatSetList(); } } } From 3a9b163aca791a9c8d54abcbf39b8a08c5904efd Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Wed, 20 Dec 2023 18:05:14 +0000 Subject: [PATCH 007/522] add delete hack :( --- public/scripts/extensions/quick-reply/index.js | 1 - .../extensions/quick-reply/src/ui/SettingsUi.js | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/public/scripts/extensions/quick-reply/index.js b/public/scripts/extensions/quick-reply/index.js index 08ff88949..a06be2737 100644 --- a/public/scripts/extensions/quick-reply/index.js +++ b/public/scripts/extensions/quick-reply/index.js @@ -15,7 +15,6 @@ import { SettingsUi } from './src/ui/SettingsUi.js'; //TODO context menus //TODO move advanced QR options into own UI class //TODO slash commands -//TODO delete QR set //TODO handle replacing QR set (create->overwrite): update configs that use this set //TODO easy way to CRUD QRs and sets //TODO easy way to set global and chat sets diff --git a/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js b/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js index 1a31a6762..376e29879 100644 --- a/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js +++ b/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js @@ -211,6 +211,20 @@ export class SettingsUi { const confirmed = await callPopup(`Are you sure you want to delete the Quick Reply Set "${this.currentQrSet.name}"? This cannot be undone.`, 'confirm'); if (confirmed) { await this.currentQrSet.delete(); + //HACK should just bubble up from QuickReplySet.delete() but that would require proper or at least more comples onDelete listeners + for (let i = this.settings.config.setList.length - 1; i >= 0; i--) { + if (this.settings.config.setList[i].set == this.currentQrSet) { + this.settings.config.setList.splice(i, 1); + } + } + if (this.settings.chatConfig) { + for (let i = this.settings.chatConfig.setList.length - 1; i >= 0; i--) { + if (this.settings.config.setList[i].set == this.currentQrSet) { + this.settings.config.setList.splice(i, 1); + } + } + } + this.settings.save(); this.rerender(); } } From c71a5bb82f9408a6760671e8e5daf847f90a7a30 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Wed, 20 Dec 2023 18:12:54 +0000 Subject: [PATCH 008/522] handle overwriting QR set --- .../scripts/extensions/quick-reply/index.js | 1 - .../quick-reply/src/ui/SettingsUi.js | 34 +++++++++++-------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/public/scripts/extensions/quick-reply/index.js b/public/scripts/extensions/quick-reply/index.js index a06be2737..ba694edab 100644 --- a/public/scripts/extensions/quick-reply/index.js +++ b/public/scripts/extensions/quick-reply/index.js @@ -15,7 +15,6 @@ import { SettingsUi } from './src/ui/SettingsUi.js'; //TODO context menus //TODO move advanced QR options into own UI class //TODO slash commands -//TODO handle replacing QR set (create->overwrite): update configs that use this set //TODO easy way to CRUD QRs and sets //TODO easy way to set global and chat sets diff --git a/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js b/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js index 376e29879..430b00a92 100644 --- a/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js +++ b/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js @@ -210,23 +210,26 @@ export class SettingsUi { async deleteQrSet() { const confirmed = await callPopup(`Are you sure you want to delete the Quick Reply Set "${this.currentQrSet.name}"? This cannot be undone.`, 'confirm'); if (confirmed) { - await this.currentQrSet.delete(); - //HACK should just bubble up from QuickReplySet.delete() but that would require proper or at least more comples onDelete listeners - for (let i = this.settings.config.setList.length - 1; i >= 0; i--) { - if (this.settings.config.setList[i].set == this.currentQrSet) { + await this.doDeleteQrSet(this.currentQrSet); + this.rerender(); + } + } + async doDeleteQrSet(qrs) { + await qrs.delete(); + //TODO (HACK) should just bubble up from QuickReplySet.delete() but that would require proper or at least more comples onDelete listeners + for (let i = this.settings.config.setList.length - 1; i >= 0; i--) { + if (this.settings.config.setList[i].set == qrs) { + this.settings.config.setList.splice(i, 1); + } + } + if (this.settings.chatConfig) { + for (let i = this.settings.chatConfig.setList.length - 1; i >= 0; i--) { + if (this.settings.config.setList[i].set == qrs) { this.settings.config.setList.splice(i, 1); } } - if (this.settings.chatConfig) { - for (let i = this.settings.chatConfig.setList.length - 1; i >= 0; i--) { - if (this.settings.config.setList[i].set == this.currentQrSet) { - this.settings.config.setList.splice(i, 1); - } - } - } - this.settings.save(); - this.rerender(); } + this.settings.save(); } async addQrSet() { @@ -236,10 +239,13 @@ export class SettingsUi { if (oldQrs) { const replace = await callPopup(`A Quick Reply Set named "${name}" already exists.\nDo you want to overwrite the existing Quick Reply Set?\nThe existing set will be deleted. This cannot be undone.`, 'confirm'); if (replace) { + const idx = QuickReplySet.list.indexOf(oldQrs); + await this.doDeleteQrSet(oldQrs); const qrs = new QuickReplySet(); qrs.name = name; qrs.addQuickReply(); - QuickReplySet.list[QuickReplySet.list.indexOf(oldQrs)] = qrs; + QuickReplySet.list.splice(idx, 0, qrs); + this.rerender(); this.currentSet.value = name; this.onQrSetChange(); this.prepareGlobalSetList(); From bab0c4b0b9dbf6c58f73e8a27675e959267cc99d Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Wed, 20 Dec 2023 18:21:04 +0000 Subject: [PATCH 009/522] add linebreaks in confirm popups --- public/scripts/extensions/quick-reply/src/ui/SettingsUi.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js b/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js index 430b00a92..d16abf296 100644 --- a/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js +++ b/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js @@ -208,7 +208,7 @@ export class SettingsUi { } async deleteQrSet() { - const confirmed = await callPopup(`Are you sure you want to delete the Quick Reply Set "${this.currentQrSet.name}"? This cannot be undone.`, 'confirm'); + const confirmed = await callPopup(`Are you sure you want to delete the Quick Reply Set "${this.currentQrSet.name}"?
This cannot be undone.`, 'confirm'); if (confirmed) { await this.doDeleteQrSet(this.currentQrSet); this.rerender(); @@ -237,7 +237,7 @@ export class SettingsUi { if (name && name.length > 0) { const oldQrs = QuickReplySet.get(name); if (oldQrs) { - const replace = await callPopup(`A Quick Reply Set named "${name}" already exists.\nDo you want to overwrite the existing Quick Reply Set?\nThe existing set will be deleted. This cannot be undone.`, 'confirm'); + const replace = await callPopup(`A Quick Reply Set named "${name}" already exists.
Do you want to overwrite the existing Quick Reply Set?
The existing set will be deleted. This cannot be undone.`, 'confirm'); if (replace) { const idx = QuickReplySet.list.indexOf(oldQrs); await this.doDeleteQrSet(oldQrs); From 65e16affb7e9bf72437c72ad8fd94eb8682caef9 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Wed, 20 Dec 2023 21:44:55 +0000 Subject: [PATCH 010/522] add context menu --- .../scripts/extensions/quick-reply/index.js | 7 +- .../extensions/quick-reply/src/QuickReply.js | 41 ++++++- .../quick-reply/src/QuickReplySet.js | 4 +- .../quick-reply/src/ui/ctx/ContextMenu.js | 108 ++++++++++++++++++ .../quick-reply/src/ui/ctx/MenuHeader.js | 20 ++++ .../quick-reply/src/ui/ctx/MenuItem.js | 76 ++++++++++++ .../quick-reply/src/ui/ctx/SubMenu.js | 66 +++++++++++ .../scripts/extensions/quick-reply/style.css | 61 ++++++++++ .../scripts/extensions/quick-reply/style.less | 74 ++++++++++++ 9 files changed, 451 insertions(+), 6 deletions(-) create mode 100644 public/scripts/extensions/quick-reply/src/ui/ctx/ContextMenu.js create mode 100644 public/scripts/extensions/quick-reply/src/ui/ctx/MenuHeader.js create mode 100644 public/scripts/extensions/quick-reply/src/ui/ctx/MenuItem.js create mode 100644 public/scripts/extensions/quick-reply/src/ui/ctx/SubMenu.js diff --git a/public/scripts/extensions/quick-reply/index.js b/public/scripts/extensions/quick-reply/index.js index ba694edab..ee1a71836 100644 --- a/public/scripts/extensions/quick-reply/index.js +++ b/public/scripts/extensions/quick-reply/index.js @@ -12,7 +12,6 @@ import { SettingsUi } from './src/ui/SettingsUi.js'; //TODO popout QR button bar (allow separate popouts for each QR set?) -//TODO context menus //TODO move advanced QR options into own UI class //TODO slash commands //TODO easy way to CRUD QRs and sets @@ -61,7 +60,7 @@ const loadSets = async () => { const setList = (await response.json()).quickReplyPresets ?? []; for (const set of setList) { if (set.version == 2) { - QuickReplySet.list.push(QuickReplySet.from(set)); + QuickReplySet.list.push(QuickReplySet.from(JSON.parse(JSON.stringify(set)))); } else { const qrs = new QuickReplySet(); qrs.name = set.name; @@ -89,6 +88,10 @@ const loadSets = async () => { await qrs.save(); } } + setList.forEach((set, idx)=>{ + QuickReplySet.list[idx].qrList = set.qrList.map(it=>QuickReply.from(it)); + QuickReplySet.list[idx].init(); + }); log('sets: ', QuickReplySet.list); } }; diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index 4e27d63aa..be30e7ad9 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -3,6 +3,7 @@ import { getSortableDelay } from '../../../utils.js'; import { log, warn } from '../index.js'; import { QuickReplyContextLink } from './QuickReplyContextLink.js'; import { QuickReplySet } from './QuickReplySet.js'; +import { ContextMenu } from './ui/ctx/ContextMenu.js'; export class QuickReply { /** @@ -35,9 +36,15 @@ export class QuickReply { /**@type {HTMLElement}*/ dom; + /**@type {HTMLElement}*/ domLabel; /**@type {HTMLElement}*/ settingsDom; + get hasContext() { + return this.contextList && this.contextList.length > 0; + } + + unrender() { @@ -47,7 +54,8 @@ export class QuickReply { updateRender() { if (!this.dom) return; this.dom.title = this.title || this.message; - this.dom.textContent = this.label; + this.domLabel.textContent = this.label; + this.dom.classList[this.hasContext ? 'add' : 'remove']('qr--hasCtx'); } render() { this.unrender(); @@ -55,8 +63,37 @@ export class QuickReply { const root = document.createElement('div'); { this.dom = root; root.classList.add('qr--button'); + if (this.hasContext) { + root.classList.add('qr--hasCtx'); + } root.title = this.title || this.message; - root.textContent = this.label; + root.addEventListener('contextmenu', (evt) => { + log('contextmenu', this, this.hasContext); + if (this.hasContext) { + evt.preventDefault(); + evt.stopPropagation(); + const menu = new ContextMenu(this); + menu.show(evt); + } + }); + const lbl = document.createElement('div'); { + this.domLabel = lbl; + lbl.classList.add('qr--button-label'); + lbl.textContent = this.label; + root.append(lbl); + } + const expander = document.createElement('div'); { + expander.classList.add('qr--button-expander'); + expander.textContent = '⋮'; + expander.title = 'Open context menu'; + expander.addEventListener('click', (evt) => { + evt.stopPropagation(); + evt.preventDefault(); + const menu = new ContextMenu(this); + menu.show(evt); + }); + root.append(expander); + } root.addEventListener('click', ()=>{ if (this.message?.length > 0 && this.onExecute) { this.onExecute(this); diff --git a/public/scripts/extensions/quick-reply/src/QuickReplySet.js b/public/scripts/extensions/quick-reply/src/QuickReplySet.js index 61156750e..0d0aff585 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReplySet.js +++ b/public/scripts/extensions/quick-reply/src/QuickReplySet.js @@ -9,9 +9,9 @@ export class QuickReplySet { static from(props) { - props.qrList = props.qrList?.map(it=>QuickReply.from(it)); + props.qrList = []; //props.qrList?.map(it=>QuickReply.from(it)); const instance = Object.assign(new this(), props); - instance.init(); + // instance.init(); return instance; } diff --git a/public/scripts/extensions/quick-reply/src/ui/ctx/ContextMenu.js b/public/scripts/extensions/quick-reply/src/ui/ctx/ContextMenu.js new file mode 100644 index 000000000..f651ec7e9 --- /dev/null +++ b/public/scripts/extensions/quick-reply/src/ui/ctx/ContextMenu.js @@ -0,0 +1,108 @@ +import { QuickReply } from '../../QuickReply.js'; +import { QuickReplyContextLink } from '../../QuickReplyContextLink.js'; +import { QuickReplySet } from '../../QuickReplySet.js'; +import { MenuHeader } from './MenuHeader.js'; +import { MenuItem } from './MenuItem.js'; + +export class ContextMenu { + /**@type {MenuItem[]}*/ itemList = []; + /**@type {Boolean}*/ isActive = false; + + /**@type {HTMLElement}*/ root; + /**@type {HTMLElement}*/ menu; + + + + + constructor(/**@type {QuickReply}*/qr) { + // this.itemList = items; + this.itemList = this.build(qr).children; + this.itemList.forEach(item => { + item.onExpand = () => { + this.itemList.filter(it => it != item) + .forEach(it => it.collapse()); + }; + }); + } + + /** + * @param {QuickReply} qr + * @param {String} chainedMessage + * @param {QuickReplySet[]} hierarchy + * @param {String[]} labelHierarchy + */ + build(qr, chainedMessage = null, hierarchy = [], labelHierarchy = []) { + const tree = { + label: qr.label, + message: (chainedMessage && qr.message ? `${chainedMessage} | ` : '') + qr.message, + children: [], + }; + qr.contextList.forEach((cl) => { + if (!hierarchy.includes(cl.set)) { + const nextHierarchy = [...hierarchy, cl.set]; + const nextLabelHierarchy = [...labelHierarchy, tree.label]; + tree.children.push(new MenuHeader(cl.set.name)); + cl.set.qrList.forEach(subQr => { + const subTree = this.build(subQr, cl.isChained ? tree.message : null, nextHierarchy, nextLabelHierarchy); + tree.children.push(new MenuItem( + subTree.label, + subTree.message, + (evt) => { + evt.stopPropagation(); + const finalQr = Object.assign(new QuickReply(), subQr); + finalQr.message = subTree.message.replace(/%%parent(-\d+)?%%/g, (_, index) => { + return nextLabelHierarchy.slice(parseInt(index ?? '-1'))[0]; + }); + cl.set.execute(finalQr); + }, + subTree.children, + )); + }); + } + }); + return tree; + } + + render() { + if (!this.root) { + const blocker = document.createElement('div'); { + this.root = blocker; + blocker.classList.add('ctx-blocker'); + blocker.addEventListener('click', () => this.hide()); + const menu = document.createElement('ul'); { + this.menu = menu; + menu.classList.add('list-group'); + menu.classList.add('ctx-menu'); + this.itemList.forEach(it => menu.append(it.render())); + blocker.append(menu); + } + } + } + return this.root; + } + + + + + show({ clientX, clientY }) { + if (this.isActive) return; + this.isActive = true; + this.render(); + this.menu.style.bottom = `${window.innerHeight - clientY}px`; + this.menu.style.left = `${clientX}px`; + document.body.append(this.root); + } + hide() { + if (this.root) { + this.root.remove(); + } + this.isActive = false; + } + toggle(/**@type {PointerEvent}*/evt) { + if (this.isActive) { + this.hide(); + } else { + this.show(evt); + } + } +} diff --git a/public/scripts/extensions/quick-reply/src/ui/ctx/MenuHeader.js b/public/scripts/extensions/quick-reply/src/ui/ctx/MenuHeader.js new file mode 100644 index 000000000..f1f38c83c --- /dev/null +++ b/public/scripts/extensions/quick-reply/src/ui/ctx/MenuHeader.js @@ -0,0 +1,20 @@ +import { MenuItem } from './MenuItem.js'; + +export class MenuHeader extends MenuItem { + constructor(/**@type {String}*/label) { + super(label, null, null); + } + + + render() { + if (!this.root) { + const item = document.createElement('li'); { + this.root = item; + item.classList.add('list-group-item'); + item.classList.add('ctx-header'); + item.append(this.label); + } + } + return this.root; + } +} diff --git a/public/scripts/extensions/quick-reply/src/ui/ctx/MenuItem.js b/public/scripts/extensions/quick-reply/src/ui/ctx/MenuItem.js new file mode 100644 index 000000000..d72fad310 --- /dev/null +++ b/public/scripts/extensions/quick-reply/src/ui/ctx/MenuItem.js @@ -0,0 +1,76 @@ +import { SubMenu } from './SubMenu.js'; + +export class MenuItem { + /**@type {String}*/ label; + /**@type {Object}*/ value; + /**@type {Function}*/ callback; + /**@type {MenuItem[]}*/ childList = []; + /**@type {SubMenu}*/ subMenu; + /**@type {Boolean}*/ isForceExpanded = false; + + /**@type {HTMLElement}*/ root; + + /**@type {Function}*/ onExpand; + + + + + constructor(/**@type {String}*/label, /**@type {Object}*/value, /**@type {function}*/callback, /**@type {MenuItem[]}*/children = []) { + this.label = label; + this.value = value; + this.callback = callback; + this.childList = children; + } + + + render() { + if (!this.root) { + const item = document.createElement('li'); { + this.root = item; + item.classList.add('list-group-item'); + item.classList.add('ctx-item'); + item.title = this.value; + if (this.callback) { + item.addEventListener('click', (evt) => this.callback(evt, this)); + } + item.append(this.label); + if (this.childList.length > 0) { + item.classList.add('ctx-has-children'); + const sub = new SubMenu(this.childList); + this.subMenu = sub; + const trigger = document.createElement('div'); { + trigger.classList.add('ctx-expander'); + trigger.textContent = '⋮'; + trigger.addEventListener('click', (evt) => { + evt.stopPropagation(); + this.toggle(); + }); + item.append(trigger); + } + item.addEventListener('mouseover', () => sub.show(item)); + item.addEventListener('mouseleave', () => sub.hide()); + + } + } + } + return this.root; + } + + + expand() { + this.subMenu?.show(this.root); + if (this.onExpand) { + this.onExpand(); + } + } + collapse() { + this.subMenu?.hide(); + } + toggle() { + if (this.subMenu.isActive) { + this.expand(); + } else { + this.collapse(); + } + } +} diff --git a/public/scripts/extensions/quick-reply/src/ui/ctx/SubMenu.js b/public/scripts/extensions/quick-reply/src/ui/ctx/SubMenu.js new file mode 100644 index 000000000..a018c60af --- /dev/null +++ b/public/scripts/extensions/quick-reply/src/ui/ctx/SubMenu.js @@ -0,0 +1,66 @@ +/** + * @typedef {import('./MenuItem.js').MenuItem} MenuItem + */ + +export class SubMenu { + /**@type {MenuItem[]}*/ itemList = []; + /**@type {Boolean}*/ isActive = false; + + /**@type {HTMLElement}*/ root; + + + + + constructor(/**@type {MenuItem[]}*/items) { + this.itemList = items; + } + + render() { + if (!this.root) { + const menu = document.createElement('ul'); { + this.root = menu; + menu.classList.add('list-group'); + menu.classList.add('ctx-menu'); + menu.classList.add('ctx-sub-menu'); + this.itemList.forEach(it => menu.append(it.render())); + } + } + return this.root; + } + + + + + show(/**@type {HTMLElement}*/parent) { + if (this.isActive) return; + this.isActive = true; + this.render(); + parent.append(this.root); + requestAnimationFrame(() => { + const rect = this.root.getBoundingClientRect(); + console.log(window.innerHeight, rect); + if (rect.bottom > window.innerHeight - 5) { + this.root.style.top = `${window.innerHeight - 5 - rect.bottom}px`; + } + if (rect.right > window.innerWidth - 5) { + this.root.style.left = 'unset'; + this.root.style.right = '100%'; + } + }); + } + hide() { + if (this.root) { + this.root.remove(); + this.root.style.top = ''; + this.root.style.left = ''; + } + this.isActive = false; + } + toggle(/**@type {HTMLElement}*/parent) { + if (this.isActive) { + this.hide(); + } else { + this.show(parent); + } + } +} diff --git a/public/scripts/extensions/quick-reply/style.css b/public/scripts/extensions/quick-reply/style.css index 970b2162a..c0f169655 100644 --- a/public/scripts/extensions/quick-reply/style.css +++ b/public/scripts/extensions/quick-reply/style.css @@ -42,6 +42,67 @@ opacity: 1; filter: brightness(1.2); } +#qr--bar > .qr--buttons .qr--button > .qr--button-expander { + display: none; +} +#qr--bar > .qr--buttons .qr--button.qr--hasCtx > .qr--button-expander { + display: block; +} +.qr--button-expander { + border-left: 1px solid; + margin-left: 1em; + text-align: center; + width: 2em; +} +.qr--button-expander:hover { + font-weight: bold; +} +.ctx-blocker { + /* backdrop-filter: blur(1px); */ + /* background-color: rgba(0 0 0 / 10%); */ + bottom: 0; + left: 0; + position: fixed; + right: 0; + top: 0; + z-index: 999; +} +.ctx-menu { + position: absolute; + overflow: visible; +} +.list-group .list-group-item.ctx-header { + font-weight: bold; + cursor: default; +} +.ctx-item + .ctx-header { + border-top: 1px solid; +} +.ctx-item { + position: relative; +} +.ctx-expander { + border-left: 1px solid; + margin-left: 1em; + text-align: center; + width: 2em; +} +.ctx-expander:hover { + font-weight: bold; +} +.ctx-sub-menu { + position: absolute; + top: 0; + left: 100%; +} +@media screen and (max-width: 1000px) { + .ctx-blocker { + position: absolute; + } + .list-group .list-group-item.ctx-item { + padding: 1em; + } +} #qr--settings .qr--head { display: flex; align-items: baseline; diff --git a/public/scripts/extensions/quick-reply/style.less b/public/scripts/extensions/quick-reply/style.less index 9a7f14916..a119e31fb 100644 --- a/public/scripts/extensions/quick-reply/style.less +++ b/public/scripts/extensions/quick-reply/style.less @@ -42,10 +42,84 @@ opacity: 1; filter: brightness(1.2); } + > .qr--button-expander { + display: none; + } + &.qr--hasCtx { + > .qr--button-expander { + display: block; + } + } } } } +.qr--button-expander { + border-left: 1px solid; + margin-left: 1em; + text-align: center; + width: 2em; + &:hover { + font-weight: bold; + } +} + +.ctx-blocker { + /* backdrop-filter: blur(1px); */ + /* background-color: rgba(0 0 0 / 10%); */ + bottom: 0; + left: 0; + position: fixed; + right: 0; + top: 0; + z-index: 999; +} + +.ctx-menu { + position: absolute; + overflow: visible; +} + +.list-group .list-group-item.ctx-header { + font-weight: bold; + cursor: default; +} + +.ctx-item+.ctx-header { + border-top: 1px solid; +} + +.ctx-item { + position: relative; +} + +.ctx-expander { + border-left: 1px solid; + margin-left: 1em; + text-align: center; + width: 2em; +} + +.ctx-expander:hover { + font-weight: bold; +} + +.ctx-sub-menu { + position: absolute; + top: 0; + left: 100%; +} + +@media screen and (max-width: 1000px) { + .ctx-blocker { + position: absolute; + } + + .list-group .list-group-item.ctx-item { + padding: 1em; + } +} + #qr--settings { From 40706e8430379336ef084481e4e20ac14d2ab53e Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Wed, 20 Dec 2023 21:53:36 +0000 Subject: [PATCH 011/522] fix isCombined not saved --- public/scripts/extensions/quick-reply/src/QuickReplySettings.js | 1 + 1 file changed, 1 insertion(+) diff --git a/public/scripts/extensions/quick-reply/src/QuickReplySettings.js b/public/scripts/extensions/quick-reply/src/QuickReplySettings.js index 8c6cba420..e89f7bd90 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReplySettings.js +++ b/public/scripts/extensions/quick-reply/src/QuickReplySettings.js @@ -76,6 +76,7 @@ export class QuickReplySettings { toJSON() { return { isEnabled: this.isEnabled, + isCombined: this.isCombined, config: this.config, }; } From 8959c0d3801e8c3e99e5996fc8f13708598c133c Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Thu, 21 Dec 2023 16:58:44 +0000 Subject: [PATCH 012/522] add QR popout --- .../scripts/extensions/quick-reply/index.js | 1 - .../extensions/quick-reply/src/QuickReply.js | 1 + .../quick-reply/src/QuickReplySettings.js | 2 + .../extensions/quick-reply/src/ui/ButtonUi.js | 133 +++++++++++++++--- .../scripts/extensions/quick-reply/style.css | 46 +++++- .../scripts/extensions/quick-reply/style.less | 34 ++++- 6 files changed, 189 insertions(+), 28 deletions(-) diff --git a/public/scripts/extensions/quick-reply/index.js b/public/scripts/extensions/quick-reply/index.js index ee1a71836..461e918b3 100644 --- a/public/scripts/extensions/quick-reply/index.js +++ b/public/scripts/extensions/quick-reply/index.js @@ -11,7 +11,6 @@ import { SettingsUi } from './src/ui/SettingsUi.js'; -//TODO popout QR button bar (allow separate popouts for each QR set?) //TODO move advanced QR options into own UI class //TODO slash commands //TODO easy way to CRUD QRs and sets diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index be30e7ad9..8e262cee2 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -63,6 +63,7 @@ export class QuickReply { const root = document.createElement('div'); { this.dom = root; root.classList.add('qr--button'); + root.classList.add('menu_button'); if (this.hasContext) { root.classList.add('qr--hasCtx'); } diff --git a/public/scripts/extensions/quick-reply/src/QuickReplySettings.js b/public/scripts/extensions/quick-reply/src/QuickReplySettings.js index e89f7bd90..caf74fcc9 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReplySettings.js +++ b/public/scripts/extensions/quick-reply/src/QuickReplySettings.js @@ -15,6 +15,7 @@ export class QuickReplySettings { /**@type {Boolean}*/ isEnabled = false; /**@type {Boolean}*/ isCombined = false; + /**@type {Boolean}*/ isPopout = false; /**@type {QuickReplyConfig}*/ config; /**@type {QuickReplyConfig}*/ _chatConfig; get chatConfig() { @@ -77,6 +78,7 @@ export class QuickReplySettings { return { isEnabled: this.isEnabled, isCombined: this.isCombined, + isPopout: this.isPopout, config: this.config, }; } diff --git a/public/scripts/extensions/quick-reply/src/ui/ButtonUi.js b/public/scripts/extensions/quick-reply/src/ui/ButtonUi.js index ede8a553f..3acb1f336 100644 --- a/public/scripts/extensions/quick-reply/src/ui/ButtonUi.js +++ b/public/scripts/extensions/quick-reply/src/ui/ButtonUi.js @@ -1,9 +1,13 @@ +import { animation_duration } from '../../../../../script.js'; +import { dragElement } from '../../../../RossAscends-mods.js'; +import { loadMovingUIState } from '../../../../power-user.js'; import { QuickReplySettings } from '../QuickReplySettings.js'; export class ButtonUi { /**@type {QuickReplySettings}*/ settings; /**@type {HTMLElement}*/ dom; + /**@type {HTMLElement}*/ popoutDom; @@ -16,6 +20,46 @@ export class ButtonUi { render() { + if (this.settings.isPopout) { + return this.renderPopout(); + } + return this.renderBar(); + } + unrender() { + this.dom?.remove(); + this.dom = null; + this.popoutDom?.remove(); + this.popoutDom = null; + } + + show() { + if (!this.settings.isEnabled) return; + if (this.settings.isPopout) { + document.body.append(this.render()); + loadMovingUIState(); + $(this.render()).fadeIn(animation_duration); + dragElement($(this.render())); + } else { + const sendForm = document.querySelector('#send_form'); + if (sendForm.children.length > 0) { + sendForm.children[0].insertAdjacentElement('beforebegin', this.render()); + } else { + sendForm.append(this.render()); + } + } + } + hide() { + this.unrender(); + } + refresh() { + this.hide(); + this.show(); + } + + + + + renderBar() { if (!this.dom) { let buttonHolder; const root = document.createElement('div'); { @@ -24,6 +68,18 @@ export class ButtonUi { root.id = 'qr--bar'; root.classList.add('flex-container'); root.classList.add('flexGap5'); + const popout = document.createElement('div'); { + popout.id = 'qr--popoutTrigger'; + popout.classList.add('menu_button'); + popout.classList.add('fa-solid'); + popout.classList.add('fa-window-restore'); + popout.addEventListener('click', ()=>{ + this.settings.isPopout = true; + this.refresh(); + this.settings.save(); + }); + root.append(popout); + } if (this.settings.isCombined) { const buttons = document.createElement('div'); { buttonHolder = buttons; @@ -39,25 +95,66 @@ export class ButtonUi { } return this.dom; } - unrender() { - this.dom?.remove(); - this.dom = null; - } - show() { - if (!this.settings.isEnabled) return; - const sendForm = document.querySelector('#send_form'); - if (sendForm.children.length > 0) { - sendForm.children[0].insertAdjacentElement('beforebegin', this.render()); - } else { - sendForm.append(this.render()); + + + + renderPopout() { + if (!this.popoutDom) { + let buttonHolder; + const root = document.createElement('div'); { + this.popoutDom = root; + root.id = 'qr--popout'; + root.classList.add('qr--popout'); + root.classList.add('draggable'); + const head = document.createElement('div'); { + head.classList.add('qr--header'); + root.append(head); + const controls = document.createElement('div'); { + controls.classList.add('qr--controls'); + controls.classList.add('panelControlBar'); + controls.classList.add('flex-container'); + const drag = document.createElement('div'); { + drag.id = 'qr--popoutheader'; + drag.classList.add('fa-solid'); + drag.classList.add('fa-grip'); + drag.classList.add('drag-grabber'); + drag.classList.add('hoverglow'); + controls.append(drag); + } + const close = document.createElement('div'); { + close.classList.add('qr--close'); + close.classList.add('fa-solid'); + close.classList.add('fa-circle-xmark'); + close.classList.add('hoverglow'); + close.addEventListener('click', ()=>{ + this.settings.isPopout = false; + this.refresh(); + this.settings.save(); + }); + controls.append(close); + } + head.append(controls); + } + } + const body = document.createElement('div'); { + buttonHolder = body; + body.classList.add('qr--body'); + if (this.settings.isCombined) { + const buttons = document.createElement('div'); { + buttonHolder = buttons; + buttons.classList.add('qr--buttons'); + body.append(buttons); + } + } + [...this.settings.config.setList, ...(this.settings.chatConfig?.setList ?? [])] + .filter(link=>link.isVisible) + .forEach(link=>buttonHolder.append(link.set.render())) + ; + root.append(body); + } + } } - } - hide() { - this.unrender(); - } - refresh() { - this.hide(); - this.show(); + return this.popoutDom; } } diff --git a/public/scripts/extensions/quick-reply/style.css b/public/scripts/extensions/quick-reply/style.css index c0f169655..91230bca3 100644 --- a/public/scripts/extensions/quick-reply/style.css +++ b/public/scripts/extensions/quick-reply/style.css @@ -10,9 +10,37 @@ max-width: 100%; overflow-x: auto; order: 1; + padding-right: 2.5em; position: relative; } -#qr--bar > .qr--buttons { +#qr--bar > #qr--popoutTrigger { + position: absolute; + right: 0.25em; + top: 0.25em; +} +#qr--popout { + display: flex; + flex-direction: column; + padding: 0; + z-index: 31; +} +#qr--popout > .qr--header { + flex: 0 0 auto; + height: 2em; + position: relative; +} +#qr--popout > .qr--header > .qr--controls > .qr--close { + height: 15px; + aspect-ratio: 1 / 1; + font-size: 20px; + opacity: 0.5; + transition: all 250ms; +} +#qr--popout > .qr--body { + overflow-y: auto; +} +#qr--bar > .qr--buttons, +#qr--popout > .qr--body > .qr--buttons { margin: 0; padding: 0; display: flex; @@ -21,12 +49,13 @@ gap: 5px; width: 100%; } -#qr--bar > .qr--buttons > .qr--buttons { +#qr--bar > .qr--buttons > .qr--buttons, +#qr--popout > .qr--body > .qr--buttons > .qr--buttons { display: contents; } -#qr--bar > .qr--buttons .qr--button { +#qr--bar > .qr--buttons .qr--button, +#qr--popout > .qr--body > .qr--buttons .qr--button { color: var(--SmartThemeBodyColor); - background-color: var(--black50a); border: 1px solid var(--SmartThemeBorderColor); border-radius: 10px; padding: 3px 5px; @@ -38,14 +67,17 @@ justify-content: center; text-align: center; } -#qr--bar > .qr--buttons .qr--button:hover { +#qr--bar > .qr--buttons .qr--button:hover, +#qr--popout > .qr--body > .qr--buttons .qr--button:hover { opacity: 1; filter: brightness(1.2); } -#qr--bar > .qr--buttons .qr--button > .qr--button-expander { +#qr--bar > .qr--buttons .qr--button > .qr--button-expander, +#qr--popout > .qr--body > .qr--buttons .qr--button > .qr--button-expander { display: none; } -#qr--bar > .qr--buttons .qr--button.qr--hasCtx > .qr--button-expander { +#qr--bar > .qr--buttons .qr--button.qr--hasCtx > .qr--button-expander, +#qr--popout > .qr--body > .qr--buttons .qr--button.qr--hasCtx > .qr--button-expander { display: block; } .qr--button-expander { diff --git a/public/scripts/extensions/quick-reply/style.less b/public/scripts/extensions/quick-reply/style.less index a119e31fb..abf8cb359 100644 --- a/public/scripts/extensions/quick-reply/style.less +++ b/public/scripts/extensions/quick-reply/style.less @@ -10,8 +10,38 @@ max-width: 100%; overflow-x: auto; order: 1; + padding-right: 2.5em; position: relative; - + > #qr--popoutTrigger { + position: absolute; + right: 0.25em; + top: 0.25em; + } +} +#qr--popout { + display: flex; + flex-direction: column; + padding: 0; + z-index: 31; + > .qr--header { + flex: 0 0 auto; + height: 2em; + position: relative; + > .qr--controls { + > .qr--close { + height: 15px; + aspect-ratio: 1 / 1; + font-size: 20px; + opacity: 0.5; + transition: all 250ms; + } + } + } + > .qr--body { + overflow-y: auto; + } +} +#qr--bar, #qr--popout > .qr--body { > .qr--buttons { margin: 0; padding: 0; @@ -27,7 +57,7 @@ .qr--button { color: var(--SmartThemeBodyColor); - background-color: var(--black50a); + // background-color: var(--black50a); border: 1px solid var(--SmartThemeBorderColor); border-radius: 10px; padding: 3px 5px; From 5125eaf1dc47394e5bd7686af8b52c4703249fb6 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Thu, 21 Dec 2023 18:44:58 +0000 Subject: [PATCH 013/522] implement slash commands --- .../scripts/extensions/quick-reply/index.js | 5 +- .../extensions/quick-reply/src/QuickReply.js | 98 ++++---- .../quick-reply/src/QuickReplyConfig.js | 37 ++- .../quick-reply/src/SlashCommandHandler.js | 217 ++++++++++++++++++ 4 files changed, 309 insertions(+), 48 deletions(-) create mode 100644 public/scripts/extensions/quick-reply/src/SlashCommandHandler.js diff --git a/public/scripts/extensions/quick-reply/index.js b/public/scripts/extensions/quick-reply/index.js index 461e918b3..b2c714b07 100644 --- a/public/scripts/extensions/quick-reply/index.js +++ b/public/scripts/extensions/quick-reply/index.js @@ -5,6 +5,7 @@ import { QuickReplyConfig } from './src/QuickReplyConfig.js'; import { QuickReplyContextLink } from './src/QuickReplyContextLink.js'; import { QuickReplySet } from './src/QuickReplySet.js'; import { QuickReplySettings } from './src/QuickReplySettings.js'; +import { SlashCommandHandler } from './src/SlashCommandHandler.js'; import { ButtonUi } from './src/ui/ButtonUi.js'; import { SettingsUi } from './src/ui/SettingsUi.js'; @@ -12,7 +13,6 @@ import { SettingsUi } from './src/ui/SettingsUi.js'; //TODO move advanced QR options into own UI class -//TODO slash commands //TODO easy way to CRUD QRs and sets //TODO easy way to set global and chat sets @@ -151,6 +151,9 @@ const init = async () => { await qr.onExecute(); } } + + const slash = new SlashCommandHandler(settings); + slash.init(); }; eventSource.on(event_types.APP_READY, init); diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index 8e262cee2..7fbb85422 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -108,9 +108,6 @@ export class QuickReply { - /** - * @param {any} idx - */ renderSettings(idx) { if (!this.settingsDom) { const item = document.createElement('div'); { @@ -194,44 +191,6 @@ export class QuickReply { this.settingsDom?.remove(); } - - - - delete() { - if (this.onDelete) { - this.unrender(); - this.unrenderSettings(); - this.onDelete(this); - } - } - /** - * @param {string} value - */ - updateMessage(value) { - if (this.onUpdate) { - this.message = value; - this.updateRender(); - this.onUpdate(this); - } - } - /** - * @param {string} value - */ - updateLabel(value) { - if (this.onUpdate) { - this.label = value; - this.updateRender(); - this.onUpdate(this); - } - } - - updateContext() { - if (this.onUpdate) { - this.updateRender(); - this.onUpdate(this); - } - } - async showOptions() { const response = await fetch('/scripts/extensions/quick-reply/html/qrOptions.html', { cache: 'no-store' }); if (response.ok) { @@ -359,6 +318,63 @@ export class QuickReply { + delete() { + if (this.onDelete) { + this.unrender(); + this.unrenderSettings(); + this.onDelete(this); + } + } + + /** + * @param {string} value + */ + updateMessage(value) { + if (this.onUpdate) { + this.message = value; + this.updateRender(); + this.onUpdate(this); + } + } + + /** + * @param {string} value + */ + updateLabel(value) { + if (this.onUpdate) { + this.label = value; + this.updateRender(); + this.onUpdate(this); + } + } + + updateContext() { + if (this.onUpdate) { + this.updateRender(); + this.onUpdate(this); + } + } + addContextLink(cl) { + this.contextList.push(cl); + this.updateContext(); + } + removeContextLink(setName) { + const idx = this.contextList.findIndex(it=>it.set.name == setName); + if (idx > -1) { + this.contextList.splice(idx, 1); + this.updateContext(); + } + } + clearContextLinks() { + if (this.contextList.length) { + this.contextList.splice(0, this.contextList.length); + this.updateContext(); + } + } + + + + toJSON() { return { id: this.id, diff --git a/public/scripts/extensions/quick-reply/src/QuickReplyConfig.js b/public/scripts/extensions/quick-reply/src/QuickReplyConfig.js index c7ca95c5a..872e33e30 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReplyConfig.js +++ b/public/scripts/extensions/quick-reply/src/QuickReplyConfig.js @@ -30,27 +30,52 @@ export class QuickReplyConfig { } + hasSet(qrs) { + return this.setList.find(it=>it.set == qrs) != null; + } + addSet(qrs, isVisible = true) { + if (!this.hasSet(qrs)) { + const qrl = new QuickReplySetLink(); + qrl.set = qrs; + qrl.isVisible = isVisible; + this.setList.push(qrl); + this.update(); + this.updateSetListDom(); + } + } + removeSet(qrs) { + const idx = this.setList.findIndex(it=>it.set == qrs); + if (idx > -1) { + this.setList.splice(idx, 1); + this.update(); + this.updateSetListDom(); + } + } + + renderSettingsInto(/**@type {HTMLElement}*/root) { /**@type {HTMLElement}*/ - const setList = root.querySelector('.qr--setList'); - this.setListDom = setList; - setList.innerHTML = ''; + this.setListDom = root.querySelector('.qr--setList'); root.querySelector('.qr--setListAdd').addEventListener('click', ()=>{ const qrl = new QuickReplySetLink(); qrl.set = QuickReplySet.list[0]; this.hookQuickReplyLink(qrl); this.setList.push(qrl); - setList.append(qrl.renderSettings(this.setList.length - 1)); + this.setListDom.append(qrl.renderSettings(this.setList.length - 1)); this.update(); }); + this.updateSetListDom(); + } + updateSetListDom() { + this.setListDom.innerHTML = ''; // @ts-ignore - $(setList).sortable({ + $(this.setListDom).sortable({ delay: getSortableDelay(), stop: ()=>this.onSetListSort(), }); - this.setList.filter(it=>!it.set.isDeleted).forEach((qrl,idx)=>setList.append(qrl.renderSettings(idx))); + this.setList.filter(it=>!it.set.isDeleted).forEach((qrl,idx)=>this.setListDom.append(qrl.renderSettings(idx))); } diff --git a/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js b/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js new file mode 100644 index 000000000..a522730bd --- /dev/null +++ b/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js @@ -0,0 +1,217 @@ +import { registerSlashCommand } from '../../../slash-commands.js'; +import { QuickReplyContextLink } from './QuickReplyContextLink.js'; +import { QuickReplySet } from './QuickReplySet.js'; +import { QuickReplySettings } from './QuickReplySettings.js'; + +export class SlashCommandHandler { + /**@type {QuickReplySettings}*/ settings; + + + + + constructor(/**@type {QuickReplySettings}*/settings) { + this.settings = settings; + } + + + + + init() { + registerSlashCommand('qr', (_, value) => this.executeQuickReplyByIndex(Number(value)), [], '(number) – activates the specified Quick Reply', true, true); + registerSlashCommand('qrset', ()=>toastr.warning('The command /qrset has been deprecated. Use /qr-set, /qr-set-on, and /qr-set-off instead.'), [], 'DEPRECATED – The command /qrset has been deprecated. Use /qr-set, /qr-set-on, and /qr-set-off instead.', true, true); + registerSlashCommand('qr-set', (args, value)=>this.toggleGlobalSet(value, args), [], '[visible=true] (number) – toggle global QR set', true, true); + registerSlashCommand('qr-set-on', (args, value)=>this.addGlobalSet(value, args), [], '[visible=true] (number) – activate global QR set', true, true); + registerSlashCommand('qr-set-off', (_, value)=>this.removeGlobalSet(value), [], '(number) – deactivate global QR set', true, true); + registerSlashCommand('qr-chat-set', (args, value)=>this.toggleChatSet(value, args), [], '[visible=true] (number) – toggle chat QR set', true, true); + registerSlashCommand('qr-chat-set-on', (args, value)=>this.addChatSet(value, args), [], '[visible=true] (number) – activate chat QR set', true, true); + registerSlashCommand('qr-chat-set-off', (_, value)=>this.removeChatSet(value), [], '(number) – deactivate chat QR set', true, true); + + const qrArgs = ` + label - string - text on the button, e.g., label=MyButton + set - string - name of the QR set, e.g., set=PresetName1 + hidden - bool - whether the button should be hidden, e.g., hidden=true + startup - bool - auto execute on app startup, e.g., startup=true + user - bool - auto execute on user message, e.g., user=true + bot - bool - auto execute on AI message, e.g., bot=true + load - bool - auto execute on chat load, e.g., load=true + title - bool - title / tooltip to be shown on button, e.g., title="My Fancy Button" + `.trim(); + const qrUpdateArgs = ` + newlabel - string - new text fort the button, e.g. newlabel=MyRenamedButton + ${qrArgs} + `.trim(); + registerSlashCommand('qr-create', (args, message)=>this.createQuickReply(args, message), [], `(arguments [message])\n arguments:\n ${qrArgs} – creates a new Quick Reply, example: /qr-create set=MyPreset label=MyButton /echo 123`, true, true); + registerSlashCommand('qr-update', (args, message)=>this.updateQuickReply(args, message), [], `(arguments [message])\n arguments:\n ${qrUpdateArgs} – updates Quick Reply, example: /qr-update set=MyPreset label=MyButton newlabel=MyRenamedButton /echo 123`, true, true); + registerSlashCommand('qr-delete', (args, name)=>this.deleteQuickReply(args, name), [], '(set=string [label]) – deletes Quick Reply', true, true); + registerSlashCommand('qr-contextadd', (args, name)=>this.createContextItem(args, name), [], '(set=string label=string chain=bool [preset name]) – add context menu preset to a QR, example: /qr-contextadd set=MyPreset label=MyButton chain=true MyOtherPreset', true, true); + registerSlashCommand('qr-contextdel', (args, name)=>this.deleteContextItem(args, name), [], '(set=string label=string [preset name]) – remove context menu preset from a QR, example: /qr-contextdel set=MyPreset label=MyButton MyOtherPreset', true, true); + registerSlashCommand('qr-contextclear', (args, label)=>this.clearContextMenu(args, label), [], '(set=string [label]) – remove all context menu presets from a QR, example: /qr-contextclear set=MyPreset MyButton', true, true); + const presetArgs = ` + enabled - bool - enable or disable the preset + nosend - bool - disable send / insert in user input (invalid for slash commands) + before - bool - place QR before user input + slots - int - number of slots + inject - bool - inject user input automatically (if disabled use {{input}}) + `.trim(); + registerSlashCommand('qr-set-create', (args, name)=>this.createSet(name, args), ['qr-presetadd'], `(arguments [label])\n arguments:\n ${presetArgs} – create a new preset (overrides existing ones), example: /qr-presetadd slots=3 MyNewPreset`, true, true); + registerSlashCommand('qr-set-update', (args, name)=>this.updateSet(name, args), ['qr-presetupdate'], `(arguments [label])\n arguments:\n ${presetArgs} – update an existing preset, example: /qr-presetupdate enabled=false MyPreset`, true, true); + } + + + + + getSetByName(name) { + const set = QuickReplySet.get(name); + if (!set) { + toastr.error(`No Quick Reply Set with the name "${name}" could be found.`); + } + return set; + } + + getQrByLabel(setName, label) { + const set = this.getSetByName(setName); + if (!set) return; + const qr = set.qrList.find(it=>it.label == label); + if (!qr) { + toastr.error(`No Quick Reply with the label "${label}" could be found in the set "${set.name}"`); + } + return qr; + } + + + + + async executeQuickReplyByIndex(idx) { + const qr = [...this.settings.config.setList, ...(this.settings.chatConfig?.setList ?? [])] + .map(it=>it.set.qrList) + .flat()[idx] + ; + if (qr) { + return await qr.onExecute(); + } else { + toastr.error(`No Quick Reply at index "${idx}"`); + } + } + + + toggleGlobalSet(name, args = {}) { + const set = this.getSetByName(name); + if (!set) return; + if (this.settings.config.hasSet(set)) { + this.settings.config.removeSet(set); + } else { + this.settings.config.addSet(set, JSON.parse(args.visible ?? 'true')); + } + } + addGlobalSet(name, args = {}) { + const set = this.getSetByName(name); + if (!set) return; + this.settings.config.addSet(set, JSON.parse(args.visible ?? 'true')); + } + removeGlobalSet(name) { + const set = this.getSetByName(name); + if (!set) return; + this.settings.config.removeSet(set); + } + + + toggleChatSet(name, args = {}) { + if (!this.settings.chatConfig) return; + const set = this.getSetByName(name); + if (!set) return; + if (this.settings.chatConfig.hasSet(set)) { + this.settings.chatConfig.removeSet(set); + } else { + this.settings.chatConfig.addSet(set, JSON.parse(args.visible ?? 'true')); + } + } + addChatSet(name, args = {}) { + if (!this.settings.chatConfig) return; + const set = this.getSetByName(name); + if (!set) return; + this.settings.chatConfig.addSet(set, JSON.parse(args.visible ?? 'true')); + } + removeChatSet(name) { + if (!this.settings.chatConfig) return; + const set = this.getSetByName(name); + if (!set) return; + this.settings.chatConfig.removeSet(set); + } + + + createQuickReply(args, message) { + const set = this.getSetByName(args.set); + if (!set) return; + const qr = set.addQuickReply(); + qr.label = args.label ?? ''; + qr.message = message ?? ''; + qr.title = args.title ?? ''; + qr.isHidden = JSON.parse(args.hidden ?? 'false') === true; + qr.executeOnStartup = JSON.parse(args.startup ?? 'false') === true; + qr.executeOnUser = JSON.parse(args.user ?? 'false') === true; + qr.executeOnAi = JSON.parse(args.bot ?? 'false') === true; + qr.executeOnChatChange = JSON.parse(args.load ?? 'false') === true; + qr.onUpdate(); + } + updateQuickReply(args, message) { + const qr = this.getQrByLabel(args.set, args.label); + if (!qr) return; + qr.message = (message ?? '').trim().length > 0 ? message : qr.message; + qr.label = args.newlabel !== undefined ? (args.newlabel ?? '') : qr.label; + qr.title = args.title !== undefined ? (args.title ?? '') : qr.title; + qr.isHidden = args.hidden !== undefined ? (JSON.parse(args.hidden ?? 'false') === true) : qr.isHidden; + qr.executeOnStartup = args.startup !== undefined ? (JSON.parse(args.startup ?? 'false') === true) : qr.executeOnStartup; + qr.executeOnUser = args.user !== undefined ? (JSON.parse(args.user ?? 'false') === true) : qr.executeOnUser; + qr.executeOnAi = args.bot !== undefined ? (JSON.parse(args.bot ?? 'false') === true) : qr.executeOnAi; + qr.executeOnChatChange = args.load !== undefined ? (JSON.parse(args.load ?? 'false') === true) : qr.executeOnChatChange; + qr.onUpdate(); + } + deleteQuickReply(args, label) { + const qr = this.getQrByLabel(args.set, args.label ?? label); + if (!qr) return; + qr.delete(); + } + + + createContextItem(args, name) { + const qr = this.getQrByLabel(args.set, args.label); + const set = this.getSetByName(name); + if (!qr || !set) return; + const cl = new QuickReplyContextLink(); + cl.set = set; + cl.isChained = JSON.parse(args.chain ?? 'false') ?? false; + qr.addContextLink(cl); + } + deleteContextItem(args, name) { + const qr = this.getQrByLabel(args.set, args.label); + const set = this.getSetByName(name); + if (!qr || !set) return; + qr.removeContextLink(set.name); + } + clearContextMenu(args, label) { + const qr = this.getQrByLabel(args.set, args.label ?? label); + if (!qr) return; + qr.clearContextLinks(); + } + + + createSet(name, args) { + const set = new QuickReplySet(); + set.name = args.name ?? name; + set.disableSend = JSON.parse(args.nosend ?? 'false') === true; + set.placeBeforeInput = JSON.parse(args.before ?? 'false') === true; + set.injectInput = JSON.parse(args.inject ?? 'false') === true; + QuickReplySet.list.push(set); + set.save(); + //TODO settings UI must be updated + } + updateSet(name, args) { + const set = this.getSetByName(args.name ?? name); + if (!set) return; + set.disableSend = args.nosend !== undefined ? (JSON.parse(args.nosend ?? 'false') === true) : set.disableSend; + set.placeBeforeInput = args.before !== undefined ? (JSON.parse(args.before ?? 'false') === true) : set.placeBeforeInput; + set.injectInput = args.inject !== undefined ? (JSON.parse(args.inject ?? 'false') === true) : set.injectInput; + set.save(); + //TODO settings UI must be updated + } +} From a0918a3f5c92e40b7e2dfa2eea4fcaba9b00648c Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Thu, 21 Dec 2023 20:05:42 +0000 Subject: [PATCH 014/522] add QR API --- .../quick-reply/api/QuickReplyApi.js | 357 ++++++++++++++++++ .../scripts/extensions/quick-reply/index.js | 8 +- .../quick-reply/src/SlashCommandHandler.js | 188 +++++---- 3 files changed, 450 insertions(+), 103 deletions(-) create mode 100644 public/scripts/extensions/quick-reply/api/QuickReplyApi.js diff --git a/public/scripts/extensions/quick-reply/api/QuickReplyApi.js b/public/scripts/extensions/quick-reply/api/QuickReplyApi.js new file mode 100644 index 000000000..33486b5c0 --- /dev/null +++ b/public/scripts/extensions/quick-reply/api/QuickReplyApi.js @@ -0,0 +1,357 @@ +import { QuickReply } from '../src/QuickReply.js'; +import { QuickReplyContextLink } from '../src/QuickReplyContextLink.js'; +import { QuickReplySet } from '../src/QuickReplySet.js'; +import { QuickReplySettings } from '../src/QuickReplySettings.js'; + + + + +export class QuickReplyApi { + /**@type {QuickReplySettings}*/ settings; + + + + + constructor(/**@type {QuickReplySettings}*/settings) { + this.settings = settings; + } + + + + + /** + * Finds and returns an existing Quick Reply Set by its name. + * + * @param {String} name name of the quick reply set + * @returns the quick reply set, or undefined if not found + */ + getSetByName(name) { + return QuickReplySet.get(name); + } + + /** + * Finds and returns an existing Quick Reply by its set's name and its label. + * + * @param {String} setName name of the quick reply set + * @param {String} label label of the quick reply + * @returns the quick reply, or undefined if not found + */ + getQrByLabel(setName, label) { + const set = this.getSetByName(setName); + if (!set) return; + return set.qrList.find(it=>it.label == label); + } + + + + + /** + * Executes a quick reply by its index and returns the result. + * + * @param {Number} idx the index (zero-based) of the quick reply to execute + * @returns the return value of the quick reply, or undefined if not found + */ + async executeQuickReplyByIndex(idx) { + const qr = [...this.settings.config.setList, ...(this.settings.chatConfig?.setList ?? [])] + .map(it=>it.set.qrList) + .flat()[idx] + ; + if (qr) { + return await qr.onExecute(); + } else { + throw new Error(`No quick reply at index "${idx}"`); + } + } + + + /** + * Adds or removes a quick reply set to the list of globally active quick reply sets. + * + * @param {String} name the name of the set + * @param {Boolean} isVisible whether to show the set's buttons or not + */ + toggleGlobalSet(name, isVisible = true) { + const set = this.getSetByName(name); + if (!set) return; + if (this.settings.config.hasSet(set)) { + this.settings.config.removeSet(set); + } else { + this.settings.config.addSet(set, isVisible); + } + } + + /** + * Adds a quick reply set to the list of globally active quick reply sets. + * + * @param {String} name the name of the set + * @param {Boolean} isVisible whether to show the set's buttons or not + */ + addGlobalSet(name, isVisible = true) { + const set = this.getSetByName(name); + if (!set) return; + this.settings.config.addSet(set, isVisible); + } + + /** + * Removes a quick reply set from the list of globally active quick reply sets. + * + * @param {String} name the name of the set + */ + removeGlobalSet(name) { + const set = this.getSetByName(name); + if (!set) return; + this.settings.config.removeSet(set); + } + + + /** + * Adds or removes a quick reply set to the list of the current chat's active quick reply sets. + * + * @param {String} name the name of the set + * @param {Boolean} isVisible whether to show the set's buttons or not + */ + toggleChatSet(name, isVisible = true) { + if (!this.settings.chatConfig) return; + const set = this.getSetByName(name); + if (!set) return; + if (this.settings.chatConfig.hasSet(set)) { + this.settings.chatConfig.removeSet(set); + } else { + this.settings.chatConfig.addSet(set, isVisible); + } + } + + /** + * Adds a quick reply set to the list of the current chat's active quick reply sets. + * + * @param {String} name the name of the set + * @param {Boolean} isVisible whether to show the set's buttons or not + */ + addChatSet(name, isVisible = true) { + if (!this.settings.chatConfig) return; + const set = this.getSetByName(name); + if (!set) return; + this.settings.chatConfig.addSet(set, isVisible); + } + + /** + * Removes a quick reply set from the list of the current chat's active quick reply sets. + * + * @param {String} name the name of the set + */ + removeChatSet(name) { + if (!this.settings.chatConfig) return; + const set = this.getSetByName(name); + if (!set) return; + this.settings.chatConfig.removeSet(set); + } + + + /** + * Creates a new quick reply in an existing quick reply set. + * + * @param {String} setName name of the quick reply set to insert the new quick reply into + * @param {String} label label for the new quick reply (text on the button) + * @param {Object} [props] + * @param {String} [props.message] the message to be sent or slash command to be executed by the new quick reply + * @param {String} [props.title] the title / tooltip to be shown on the quick reply button + * @param {Boolean} [props.isHidden] whether to hide or show the button + * @param {Boolean} [props.executeOnStartup] whether to execute the quick reply when SillyTavern starts + * @param {Boolean} [props.executeOnUser] whether to execute the quick reply after a user has sent a message + * @param {Boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message + * @param {Boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded + * @returns {QuickReply} the new quick reply + */ + createQuickReply(setName, label, { + message, + title, + isHidden, + executeOnStartup, + executeOnUser, + executeOnAi, + executeOnChatChange, + } = {}) { + const set = this.getSetByName(setName); + if (!set) { + throw new Error(`No quick reply set with named "${setName}" found.`); + } + const qr = set.addQuickReply(); + qr.label = label ?? ''; + qr.message = message ?? ''; + qr.title = title ?? ''; + qr.isHidden = isHidden ?? false; + qr.executeOnStartup = executeOnStartup ?? false; + qr.executeOnUser = executeOnUser ?? false; + qr.executeOnAi = executeOnAi ?? false; + qr.executeOnChatChange = executeOnChatChange ?? false; + qr.onUpdate(); + return qr; + } + + /** + * Updates an existing quick reply. + * + * @param {String} setName name of the existing quick reply set + * @param {String} label label of the existing quick reply (text on the button) + * @param {Object} [props] + * @param {String} [props.newLabel] new label for quick reply (text on the button) + * @param {String} [props.message] the message to be sent or slash command to be executed by the quick reply + * @param {String} [props.title] the title / tooltip to be shown on the quick reply button + * @param {Boolean} [props.isHidden] whether to hide or show the button + * @param {Boolean} [props.executeOnStartup] whether to execute the quick reply when SillyTavern starts + * @param {Boolean} [props.executeOnUser] whether to execute the quick reply after a user has sent a message + * @param {Boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message + * @param {Boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded + * @returns {QuickReply} the altered quick reply + */ + updateQuickReply(setName, label, { + newLabel, + message, + title, + isHidden, + executeOnStartup, + executeOnUser, + executeOnAi, + executeOnChatChange, + } = {}) { + const qr = this.getQrByLabel(setName, label); + if (!qr) { + throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`); + } + qr.label = newLabel ?? qr.label; + qr.message = message ?? qr.message; + qr.title = title ?? qr.title; + qr.isHidden = isHidden ?? qr.isHidden; + qr.executeOnStartup = executeOnStartup ?? qr.executeOnStartup; + qr.executeOnUser = executeOnUser ?? qr.executeOnUser; + qr.executeOnAi = executeOnAi ?? qr.executeOnAi; + qr.executeOnChatChange = executeOnChatChange ?? qr.executeOnChatChange; + qr.onUpdate(); + return qr; + } + + /** + * Deletes an existing quick reply. + * + * @param {String} setName name of the existing quick reply set + * @param {String} label label of the existing quick reply (text on the button) + */ + deleteQuickReply(setName, label) { + const qr = this.getQrByLabel(setName, label); + if (!qr) { + throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`); + } + qr.delete(); + } + + + /** + * Adds an existing quick reply set as a context menu to an existing quick reply. + * + * @param {String} setName name of the existing quick reply set containing the quick reply + * @param {String} label label of the existing quick reply + * @param {String} contextSetName name of the existing quick reply set to be used as a context menu + * @param {Boolean} isChained whether or not to chain the context menu quick replies + */ + createContextItem(setName, label, contextSetName, isChained = false) { + const qr = this.getQrByLabel(setName, label); + const set = this.getSetByName(contextSetName); + if (!qr) { + throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`); + } + if (!set) { + throw new Error(`No quick reply set with name "${contextSetName}" found.`); + } + const cl = new QuickReplyContextLink(); + cl.set = set; + cl.isChained = isChained; + qr.addContextLink(cl); + } + + /** + * Removes a quick reply set from a quick reply's context menu. + * + * @param {String} setName name of the existing quick reply set containing the quick reply + * @param {String} label label of the existing quick reply + * @param {String} contextSetName name of the existing quick reply set to be used as a context menu + */ + deleteContextItem(setName, label, contextSetName) { + const qr = this.getQrByLabel(setName, label); + const set = this.getSetByName(contextSetName); + if (!qr) { + throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`); + } + if (!set) { + throw new Error(`No quick reply set with name "${contextSetName}" found.`); + } + qr.removeContextLink(set.name); + } + + /** + * Removes all entries from a quick reply's context menu. + * + * @param {String} setName name of the existing quick reply set containing the quick reply + * @param {String} label label of the existing quick reply + */ + clearContextMenu(setName, label) { + const qr = this.getQrByLabel(setName, label); + if (!qr) { + throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`); + } + qr.clearContextLinks(); + } + + + /** + * Create a new quick reply set. + * + * @param {String} name name of the new quick reply set + * @param {Object} [props] + * @param {Boolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box + * @param {Boolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input + * @param {Boolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply + * @returns {QuickReplySet} the new quick reply set + */ + createSet(name, { + disableSend, + placeBeforeInput, + injectInput, + } = {}) { + const set = new QuickReplySet(); + set.name = name; + set.disableSend = disableSend ?? false; + set.placeBeforeInput = placeBeforeInput ?? false; + set.injectInput = injectInput ?? false; + QuickReplySet.list.push(set); + set.save(); + //TODO settings UI must be updated + return set; + } + + /** + * Update an existing quick reply set. + * + * @param {String} name name of the existing quick reply set + * @param {Object} [props] + * @param {Boolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box + * @param {Boolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input + * @param {Boolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply + * @returns {QuickReplySet} the altered quick reply set + */ + updateSet(name, { + disableSend, + placeBeforeInput, + injectInput, + } = {}) { + const set = this.getSetByName(name); + if (!set) { + throw new Error(`No quick reply set with name "${name}" found.`); + } + set.disableSend = disableSend ?? false; + set.placeBeforeInput = placeBeforeInput ?? false; + set.injectInput = injectInput ?? false; + set.save(); + //TODO settings UI must be updated + return set; + } +} diff --git a/public/scripts/extensions/quick-reply/index.js b/public/scripts/extensions/quick-reply/index.js index b2c714b07..3c45392a9 100644 --- a/public/scripts/extensions/quick-reply/index.js +++ b/public/scripts/extensions/quick-reply/index.js @@ -1,5 +1,6 @@ import { chat_metadata, eventSource, event_types, getRequestHeaders } from '../../../script.js'; import { extension_settings } from '../../extensions.js'; +import { QuickReplyApi } from './api/QuickReplyApi.js'; import { QuickReply } from './src/QuickReply.js'; import { QuickReplyConfig } from './src/QuickReplyConfig.js'; import { QuickReplyContextLink } from './src/QuickReplyContextLink.js'; @@ -13,8 +14,6 @@ import { SettingsUi } from './src/ui/SettingsUi.js'; //TODO move advanced QR options into own UI class -//TODO easy way to CRUD QRs and sets -//TODO easy way to set global and chat sets @@ -44,6 +43,8 @@ let settings; let manager; /** @type {ButtonUi} */ let buttons; +/** @type {QuickReplyApi} */ +export let api; @@ -152,7 +153,8 @@ const init = async () => { } } - const slash = new SlashCommandHandler(settings); + api = new QuickReplyApi(settings); + const slash = new SlashCommandHandler(api); slash.init(); }; eventSource.on(event_types.APP_READY, init); diff --git a/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js b/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js index a522730bd..37f054616 100644 --- a/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js +++ b/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js @@ -1,16 +1,14 @@ import { registerSlashCommand } from '../../../slash-commands.js'; -import { QuickReplyContextLink } from './QuickReplyContextLink.js'; -import { QuickReplySet } from './QuickReplySet.js'; -import { QuickReplySettings } from './QuickReplySettings.js'; +import { QuickReplyApi } from '../api/QuickReplyApi.js'; export class SlashCommandHandler { - /**@type {QuickReplySettings}*/ settings; + /**@type {QuickReplyApi}*/ api; - constructor(/**@type {QuickReplySettings}*/settings) { - this.settings = settings; + constructor(/**@type {QuickReplyApi}*/api) { + this.api = api; } @@ -61,7 +59,7 @@ export class SlashCommandHandler { getSetByName(name) { - const set = QuickReplySet.get(name); + const set = this.api.getSetByName(name); if (!set) { toastr.error(`No Quick Reply Set with the name "${name}" could be found.`); } @@ -69,11 +67,9 @@ export class SlashCommandHandler { } getQrByLabel(setName, label) { - const set = this.getSetByName(setName); - if (!set) return; - const qr = set.qrList.find(it=>it.label == label); + const qr = this.api.getQrByLabel(setName, label); if (!qr) { - toastr.error(`No Quick Reply with the label "${label}" could be found in the set "${set.name}"`); + toastr.error(`No Quick Reply with the label "${label}" could be found in the set "${setName}"`); } return qr; } @@ -82,111 +78,102 @@ export class SlashCommandHandler { async executeQuickReplyByIndex(idx) { - const qr = [...this.settings.config.setList, ...(this.settings.chatConfig?.setList ?? [])] - .map(it=>it.set.qrList) - .flat()[idx] - ; - if (qr) { - return await qr.onExecute(); - } else { - toastr.error(`No Quick Reply at index "${idx}"`); + try { + return await this.api.executeQuickReplyByIndex(idx); + } catch (ex) { + toastr.error(ex.message); } } toggleGlobalSet(name, args = {}) { - const set = this.getSetByName(name); - if (!set) return; - if (this.settings.config.hasSet(set)) { - this.settings.config.removeSet(set); - } else { - this.settings.config.addSet(set, JSON.parse(args.visible ?? 'true')); - } + this.api.toggleGlobalSet(name, JSON.parse(args.visible ?? 'true') === true); } addGlobalSet(name, args = {}) { - const set = this.getSetByName(name); - if (!set) return; - this.settings.config.addSet(set, JSON.parse(args.visible ?? 'true')); + this.api.addGlobalSet(name, JSON.parse(args.visible ?? 'true') === true); } removeGlobalSet(name) { - const set = this.getSetByName(name); - if (!set) return; - this.settings.config.removeSet(set); + this.api.removeGlobalSet(name); } toggleChatSet(name, args = {}) { - if (!this.settings.chatConfig) return; - const set = this.getSetByName(name); - if (!set) return; - if (this.settings.chatConfig.hasSet(set)) { - this.settings.chatConfig.removeSet(set); - } else { - this.settings.chatConfig.addSet(set, JSON.parse(args.visible ?? 'true')); - } + this.api.toggleChatSet(name, JSON.parse(args.visible ?? 'true') === true); } addChatSet(name, args = {}) { - if (!this.settings.chatConfig) return; - const set = this.getSetByName(name); - if (!set) return; - this.settings.chatConfig.addSet(set, JSON.parse(args.visible ?? 'true')); + this.api.addChatSet(name, JSON.parse(args.visible ?? 'true') === true); } removeChatSet(name) { - if (!this.settings.chatConfig) return; - const set = this.getSetByName(name); - if (!set) return; - this.settings.chatConfig.removeSet(set); + this.api.removeChatSet(name); } createQuickReply(args, message) { - const set = this.getSetByName(args.set); - if (!set) return; - const qr = set.addQuickReply(); - qr.label = args.label ?? ''; - qr.message = message ?? ''; - qr.title = args.title ?? ''; - qr.isHidden = JSON.parse(args.hidden ?? 'false') === true; - qr.executeOnStartup = JSON.parse(args.startup ?? 'false') === true; - qr.executeOnUser = JSON.parse(args.user ?? 'false') === true; - qr.executeOnAi = JSON.parse(args.bot ?? 'false') === true; - qr.executeOnChatChange = JSON.parse(args.load ?? 'false') === true; - qr.onUpdate(); + try { + this.api.createQuickReply( + args.set ?? '', + args.label ?? '', + { + message: message ?? '', + title: args.title, + isHidden: JSON.parse(args.hidden ?? 'false') === true, + executeOnStartup: JSON.parse(args.startup ?? 'false') === true, + executeOnUser: JSON.parse(args.user ?? 'false') === true, + executeOnAi: JSON.parse(args.bot ?? 'false') === true, + executeOnChatChange: JSON.parse(args.load ?? 'false') === true, + }, + ); + } catch (ex) { + toastr.error(ex.message); + } } updateQuickReply(args, message) { - const qr = this.getQrByLabel(args.set, args.label); - if (!qr) return; - qr.message = (message ?? '').trim().length > 0 ? message : qr.message; - qr.label = args.newlabel !== undefined ? (args.newlabel ?? '') : qr.label; - qr.title = args.title !== undefined ? (args.title ?? '') : qr.title; - qr.isHidden = args.hidden !== undefined ? (JSON.parse(args.hidden ?? 'false') === true) : qr.isHidden; - qr.executeOnStartup = args.startup !== undefined ? (JSON.parse(args.startup ?? 'false') === true) : qr.executeOnStartup; - qr.executeOnUser = args.user !== undefined ? (JSON.parse(args.user ?? 'false') === true) : qr.executeOnUser; - qr.executeOnAi = args.bot !== undefined ? (JSON.parse(args.bot ?? 'false') === true) : qr.executeOnAi; - qr.executeOnChatChange = args.load !== undefined ? (JSON.parse(args.load ?? 'false') === true) : qr.executeOnChatChange; - qr.onUpdate(); + try { + this.api.updateQuickReply( + args.set ?? '', + args.label ?? '', + { + newLabel: args.newlabel, + message: (message ?? '').trim().length > 0 ? message : undefined, + title: args.title, + isHidden: args.hidden, + executeOnStartup: args.startup, + executeOnUser: args.user, + executeOnAi: args.bot, + executeOnChatChange: args.load, + }, + ); + } catch (ex) { + toastr.error(ex.message); + } } deleteQuickReply(args, label) { - const qr = this.getQrByLabel(args.set, args.label ?? label); - if (!qr) return; - qr.delete(); + try { + this.api.deleteQuickReply(args.set, label); + } catch (ex) { + toastr.error(ex.message); + } } createContextItem(args, name) { - const qr = this.getQrByLabel(args.set, args.label); - const set = this.getSetByName(name); - if (!qr || !set) return; - const cl = new QuickReplyContextLink(); - cl.set = set; - cl.isChained = JSON.parse(args.chain ?? 'false') ?? false; - qr.addContextLink(cl); + try { + this.api.createContextItem( + args.set, + args.label, + name, + JSON.parse(args.chain ?? 'false') === true, + ); + } catch (ex) { + toastr.error(ex.message); + } } deleteContextItem(args, name) { - const qr = this.getQrByLabel(args.set, args.label); - const set = this.getSetByName(name); - if (!qr || !set) return; - qr.removeContextLink(set.name); + try { + this.api.deleteContextItem(args.set, args.label, name); + } catch (ex) { + toastr.error(ex.message); + } } clearContextMenu(args, label) { const qr = this.getQrByLabel(args.set, args.label ?? label); @@ -196,22 +183,23 @@ export class SlashCommandHandler { createSet(name, args) { - const set = new QuickReplySet(); - set.name = args.name ?? name; - set.disableSend = JSON.parse(args.nosend ?? 'false') === true; - set.placeBeforeInput = JSON.parse(args.before ?? 'false') === true; - set.injectInput = JSON.parse(args.inject ?? 'false') === true; - QuickReplySet.list.push(set); - set.save(); - //TODO settings UI must be updated + this.api.createSet( + args.name ?? name ?? '', + { + disableSend: JSON.parse(args.nosend ?? 'false') === true, + placeBeforeInput: JSON.parse(args.before ?? 'false') === true, + injectInput: JSON.parse(args.inject ?? 'false') === true, + }, + ); } updateSet(name, args) { - const set = this.getSetByName(args.name ?? name); - if (!set) return; - set.disableSend = args.nosend !== undefined ? (JSON.parse(args.nosend ?? 'false') === true) : set.disableSend; - set.placeBeforeInput = args.before !== undefined ? (JSON.parse(args.before ?? 'false') === true) : set.placeBeforeInput; - set.injectInput = args.inject !== undefined ? (JSON.parse(args.inject ?? 'false') === true) : set.injectInput; - set.save(); - //TODO settings UI must be updated + this.api.updateSet( + args.name ?? name ?? '', + { + disableSend: args.nosend !== undefined ? JSON.parse(args.nosend ?? 'false') === true : undefined, + placeBeforeInput: args.before !== undefined ? JSON.parse(args.before ?? 'false') === true : undefined, + injectInput: args.inject !== undefined ? JSON.parse(args.inject ?? 'false') === true : undefined, + }, + ); } } From 9f13ab1fe93ca23189c97aa340f3344d53134845 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Thu, 21 Dec 2023 20:15:32 +0000 Subject: [PATCH 015/522] rename exported quick reply api var --- public/scripts/extensions/quick-reply/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/scripts/extensions/quick-reply/index.js b/public/scripts/extensions/quick-reply/index.js index 3c45392a9..93fc4b3ae 100644 --- a/public/scripts/extensions/quick-reply/index.js +++ b/public/scripts/extensions/quick-reply/index.js @@ -44,7 +44,7 @@ let manager; /** @type {ButtonUi} */ let buttons; /** @type {QuickReplyApi} */ -export let api; +export let quickReplyApi; @@ -153,8 +153,8 @@ const init = async () => { } } - api = new QuickReplyApi(settings); - const slash = new SlashCommandHandler(api); + quickReplyApi = new QuickReplyApi(settings); + const slash = new SlashCommandHandler(quickReplyApi); slash.init(); }; eventSource.on(event_types.APP_READY, init); From 9e7bc0b8ab86f284bbe72ee062bfbcd9053e09f0 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Thu, 21 Dec 2023 20:15:56 +0000 Subject: [PATCH 016/522] update todos --- public/scripts/extensions/quick-reply/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/scripts/extensions/quick-reply/index.js b/public/scripts/extensions/quick-reply/index.js index 93fc4b3ae..03e54b4d6 100644 --- a/public/scripts/extensions/quick-reply/index.js +++ b/public/scripts/extensions/quick-reply/index.js @@ -13,6 +13,7 @@ import { SettingsUi } from './src/ui/SettingsUi.js'; +//TODO slash command hints //TODO move advanced QR options into own UI class @@ -104,6 +105,7 @@ const loadSettings = async () => { try { settings = QuickReplySettings.from(extension_settings.quickReplyV2); } catch (ex) { + //TODO remove debugger debugger; settings = QuickReplySettings.from(defaultSettings); } From e3c2d6771c50376a41f7a62d05acf7a0be262ab9 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Fri, 22 Dec 2023 12:15:44 +0000 Subject: [PATCH 017/522] fix slash command hints --- .../scripts/extensions/quick-reply/index.js | 1 - .../quick-reply/src/SlashCommandHandler.js | 20 +++++++++---------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/public/scripts/extensions/quick-reply/index.js b/public/scripts/extensions/quick-reply/index.js index 03e54b4d6..edefc4490 100644 --- a/public/scripts/extensions/quick-reply/index.js +++ b/public/scripts/extensions/quick-reply/index.js @@ -13,7 +13,6 @@ import { SettingsUi } from './src/ui/SettingsUi.js'; -//TODO slash command hints //TODO move advanced QR options into own UI class diff --git a/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js b/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js index 37f054616..01aa0f7ff 100644 --- a/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js +++ b/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js @@ -35,24 +35,22 @@ export class SlashCommandHandler { title - bool - title / tooltip to be shown on button, e.g., title="My Fancy Button" `.trim(); const qrUpdateArgs = ` - newlabel - string - new text fort the button, e.g. newlabel=MyRenamedButton + newlabel - string - new text for the button, e.g. newlabel=MyRenamedButton ${qrArgs} `.trim(); - registerSlashCommand('qr-create', (args, message)=>this.createQuickReply(args, message), [], `(arguments [message])\n arguments:\n ${qrArgs} – creates a new Quick Reply, example: /qr-create set=MyPreset label=MyButton /echo 123`, true, true); - registerSlashCommand('qr-update', (args, message)=>this.updateQuickReply(args, message), [], `(arguments [message])\n arguments:\n ${qrUpdateArgs} – updates Quick Reply, example: /qr-update set=MyPreset label=MyButton newlabel=MyRenamedButton /echo 123`, true, true); - registerSlashCommand('qr-delete', (args, name)=>this.deleteQuickReply(args, name), [], '(set=string [label]) – deletes Quick Reply', true, true); - registerSlashCommand('qr-contextadd', (args, name)=>this.createContextItem(args, name), [], '(set=string label=string chain=bool [preset name]) – add context menu preset to a QR, example: /qr-contextadd set=MyPreset label=MyButton chain=true MyOtherPreset', true, true); - registerSlashCommand('qr-contextdel', (args, name)=>this.deleteContextItem(args, name), [], '(set=string label=string [preset name]) – remove context menu preset from a QR, example: /qr-contextdel set=MyPreset label=MyButton MyOtherPreset', true, true); - registerSlashCommand('qr-contextclear', (args, label)=>this.clearContextMenu(args, label), [], '(set=string [label]) – remove all context menu presets from a QR, example: /qr-contextclear set=MyPreset MyButton', true, true); + registerSlashCommand('qr-create', (args, message)=>this.createQuickReply(args, message), [], `[arguments] (message)\n arguments:\n ${qrArgs} – creates a new Quick Reply, example: /qr-create set=MyPreset label=MyButton /echo 123`, true, true); + registerSlashCommand('qr-update', (args, message)=>this.updateQuickReply(args, message), [], `[arguments] (message)\n arguments:\n ${qrUpdateArgs} – updates Quick Reply, example: /qr-update set=MyPreset label=MyButton newlabel=MyRenamedButton /echo 123`, true, true); + registerSlashCommand('qr-delete', (args, name)=>this.deleteQuickReply(args, name), [], 'set=string [label] – deletes Quick Reply', true, true); + registerSlashCommand('qr-contextadd', (args, name)=>this.createContextItem(args, name), [], 'set=string label=string [chain=false] (preset name) – add context menu preset to a QR, example: /qr-contextadd set=MyPreset label=MyButton chain=true MyOtherPreset', true, true); + registerSlashCommand('qr-contextdel', (args, name)=>this.deleteContextItem(args, name), [], 'set=string label=string (preset name) – remove context menu preset from a QR, example: /qr-contextdel set=MyPreset label=MyButton MyOtherPreset', true, true); + registerSlashCommand('qr-contextclear', (args, label)=>this.clearContextMenu(args, label), [], 'set=string (label) – remove all context menu presets from a QR, example: /qr-contextclear set=MyPreset MyButton', true, true); const presetArgs = ` - enabled - bool - enable or disable the preset nosend - bool - disable send / insert in user input (invalid for slash commands) before - bool - place QR before user input - slots - int - number of slots inject - bool - inject user input automatically (if disabled use {{input}}) `.trim(); - registerSlashCommand('qr-set-create', (args, name)=>this.createSet(name, args), ['qr-presetadd'], `(arguments [label])\n arguments:\n ${presetArgs} – create a new preset (overrides existing ones), example: /qr-presetadd slots=3 MyNewPreset`, true, true); - registerSlashCommand('qr-set-update', (args, name)=>this.updateSet(name, args), ['qr-presetupdate'], `(arguments [label])\n arguments:\n ${presetArgs} – update an existing preset, example: /qr-presetupdate enabled=false MyPreset`, true, true); + registerSlashCommand('qr-set-create', (args, name)=>this.createSet(name, args), ['qr-presetadd'], `[arguments] (label)\n arguments:\n ${presetArgs} – create a new preset (overrides existing ones), example: /qr-presetadd MyNewPreset`, true, true); + registerSlashCommand('qr-set-update', (args, name)=>this.updateSet(name, args), ['qr-presetupdate'], `[arguments] (label)\n arguments:\n ${presetArgs} – update an existing preset, example: /qr-presetupdate enabled=false MyPreset`, true, true); } From a088fb174614ca8e985ab65e649a867b9e759ddc Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Fri, 22 Dec 2023 12:55:25 +0000 Subject: [PATCH 018/522] error handling --- .../quick-reply/api/QuickReplyApi.js | 24 ++++-- .../quick-reply/src/SlashCommandHandler.js | 85 +++++++++++++------ 2 files changed, 78 insertions(+), 31 deletions(-) diff --git a/public/scripts/extensions/quick-reply/api/QuickReplyApi.js b/public/scripts/extensions/quick-reply/api/QuickReplyApi.js index 33486b5c0..c3d550c4c 100644 --- a/public/scripts/extensions/quick-reply/api/QuickReplyApi.js +++ b/public/scripts/extensions/quick-reply/api/QuickReplyApi.js @@ -72,7 +72,9 @@ export class QuickReplyApi { */ toggleGlobalSet(name, isVisible = true) { const set = this.getSetByName(name); - if (!set) return; + if (!set) { + throw new Error(`No quick reply set with name "${name}" found.`); + } if (this.settings.config.hasSet(set)) { this.settings.config.removeSet(set); } else { @@ -88,7 +90,9 @@ export class QuickReplyApi { */ addGlobalSet(name, isVisible = true) { const set = this.getSetByName(name); - if (!set) return; + if (!set) { + throw new Error(`No quick reply set with name "${name}" found.`); + } this.settings.config.addSet(set, isVisible); } @@ -99,7 +103,9 @@ export class QuickReplyApi { */ removeGlobalSet(name) { const set = this.getSetByName(name); - if (!set) return; + if (!set) { + throw new Error(`No quick reply set with name "${name}" found.`); + } this.settings.config.removeSet(set); } @@ -113,7 +119,9 @@ export class QuickReplyApi { toggleChatSet(name, isVisible = true) { if (!this.settings.chatConfig) return; const set = this.getSetByName(name); - if (!set) return; + if (!set) { + throw new Error(`No quick reply set with name "${name}" found.`); + } if (this.settings.chatConfig.hasSet(set)) { this.settings.chatConfig.removeSet(set); } else { @@ -130,7 +138,9 @@ export class QuickReplyApi { addChatSet(name, isVisible = true) { if (!this.settings.chatConfig) return; const set = this.getSetByName(name); - if (!set) return; + if (!set) { + throw new Error(`No quick reply set with name "${name}" found.`); + } this.settings.chatConfig.addSet(set, isVisible); } @@ -142,7 +152,9 @@ export class QuickReplyApi { removeChatSet(name) { if (!this.settings.chatConfig) return; const set = this.getSetByName(name); - if (!set) return; + if (!set) { + throw new Error(`No quick reply set with name "${name}" found.`); + } this.settings.chatConfig.removeSet(set); } diff --git a/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js b/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js index 01aa0f7ff..3f1ee2d2c 100644 --- a/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js +++ b/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js @@ -85,24 +85,48 @@ export class SlashCommandHandler { toggleGlobalSet(name, args = {}) { - this.api.toggleGlobalSet(name, JSON.parse(args.visible ?? 'true') === true); + try { + this.api.toggleGlobalSet(name, JSON.parse(args.visible ?? 'true') === true); + } catch (ex) { + toastr.error(ex.message); + } } addGlobalSet(name, args = {}) { - this.api.addGlobalSet(name, JSON.parse(args.visible ?? 'true') === true); + try { + this.api.addGlobalSet(name, JSON.parse(args.visible ?? 'true') === true); + } catch (ex) { + toastr.error(ex.message); + } } removeGlobalSet(name) { - this.api.removeGlobalSet(name); + try { + this.api.removeGlobalSet(name); + } catch (ex) { + toastr.error(ex.message); + } } toggleChatSet(name, args = {}) { - this.api.toggleChatSet(name, JSON.parse(args.visible ?? 'true') === true); + try { + this.api.toggleChatSet(name, JSON.parse(args.visible ?? 'true') === true); + } catch (ex) { + toastr.error(ex.message); + } } addChatSet(name, args = {}) { - this.api.addChatSet(name, JSON.parse(args.visible ?? 'true') === true); + try { + this.api.addChatSet(name, JSON.parse(args.visible ?? 'true') === true); + } catch (ex) { + toastr.error(ex.message); + } } removeChatSet(name) { - this.api.removeChatSet(name); + try { + this.api.removeChatSet(name); + } catch (ex) { + toastr.error(ex.message); + } } @@ -174,30 +198,41 @@ export class SlashCommandHandler { } } clearContextMenu(args, label) { - const qr = this.getQrByLabel(args.set, args.label ?? label); - if (!qr) return; - qr.clearContextLinks(); + try { + this.api.clearContextMenu(args.set, args.label ?? label); + } catch (ex) { + toastr.error(ex.message); + } } createSet(name, args) { - this.api.createSet( - args.name ?? name ?? '', - { - disableSend: JSON.parse(args.nosend ?? 'false') === true, - placeBeforeInput: JSON.parse(args.before ?? 'false') === true, - injectInput: JSON.parse(args.inject ?? 'false') === true, - }, - ); + try { + this.api.createSet( + args.name ?? name ?? '', + { + disableSend: JSON.parse(args.nosend ?? 'false') === true, + placeBeforeInput: JSON.parse(args.before ?? 'false') === true, + injectInput: JSON.parse(args.inject ?? 'false') === true, + }, + ); + } catch (ex) { + toastr.error(ex.message); + } } updateSet(name, args) { - this.api.updateSet( - args.name ?? name ?? '', - { - disableSend: args.nosend !== undefined ? JSON.parse(args.nosend ?? 'false') === true : undefined, - placeBeforeInput: args.before !== undefined ? JSON.parse(args.before ?? 'false') === true : undefined, - injectInput: args.inject !== undefined ? JSON.parse(args.inject ?? 'false') === true : undefined, - }, - ); + try { + this.api.updateSet( + args.name ?? name ?? '', + { + disableSend: args.nosend !== undefined ? JSON.parse(args.nosend ?? 'false') === true : undefined, + placeBeforeInput: args.before !== undefined ? JSON.parse(args.before ?? 'false') === true : undefined, + injectInput: args.inject !== undefined ? JSON.parse(args.inject ?? 'false') === true : undefined, + }, + ); + } catch (ex) { + toastr.error(ex.message); + } + } } } From 4fc456dffaa390afa3d3a4250f8fb25e677c26fd Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Fri, 22 Dec 2023 12:56:06 +0000 Subject: [PATCH 019/522] delete QR set command and API --- .../quick-reply/api/QuickReplyApi.js | 47 +++++++++++++++---- .../scripts/extensions/quick-reply/index.js | 28 ++++++++++- .../quick-reply/src/QuickReplySet.js | 5 +- .../quick-reply/src/SlashCommandHandler.js | 11 ++++- 4 files changed, 75 insertions(+), 16 deletions(-) diff --git a/public/scripts/extensions/quick-reply/api/QuickReplyApi.js b/public/scripts/extensions/quick-reply/api/QuickReplyApi.js index c3d550c4c..dc81bd263 100644 --- a/public/scripts/extensions/quick-reply/api/QuickReplyApi.js +++ b/public/scripts/extensions/quick-reply/api/QuickReplyApi.js @@ -2,18 +2,21 @@ import { QuickReply } from '../src/QuickReply.js'; import { QuickReplyContextLink } from '../src/QuickReplyContextLink.js'; import { QuickReplySet } from '../src/QuickReplySet.js'; import { QuickReplySettings } from '../src/QuickReplySettings.js'; +import { SettingsUi } from '../src/ui/SettingsUi.js'; export class QuickReplyApi { /**@type {QuickReplySettings}*/ settings; + /**@type {SettingsUi}*/ settingsUi; - constructor(/**@type {QuickReplySettings}*/settings) { + constructor(/**@type {QuickReplySettings}*/settings, /**@type {SettingsUi}*/settingsUi) { this.settings = settings; + this.settingsUi = settingsUi; } @@ -322,9 +325,9 @@ export class QuickReplyApi { * @param {Boolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box * @param {Boolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input * @param {Boolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply - * @returns {QuickReplySet} the new quick reply set + * @returns {Promise} the new quick reply set */ - createSet(name, { + async createSet(name, { disableSend, placeBeforeInput, injectInput, @@ -334,9 +337,19 @@ export class QuickReplyApi { set.disableSend = disableSend ?? false; set.placeBeforeInput = placeBeforeInput ?? false; set.injectInput = injectInput ?? false; - QuickReplySet.list.push(set); - set.save(); - //TODO settings UI must be updated + const oldSet = this.getSetByName(name); + if (oldSet) { + QuickReplySet.list.splice(QuickReplySet.list.indexOf(oldSet), 1, set); + } else { + const idx = QuickReplySet.list.findIndex(it=>it.name.localeCompare(name) == 1); + if (idx > -1) { + QuickReplySet.list.splice(idx, 0, set); + } else { + QuickReplySet.list.push(set); + } + } + await set.save(); + this.settingsUi.rerender(); return set; } @@ -348,9 +361,9 @@ export class QuickReplyApi { * @param {Boolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box * @param {Boolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input * @param {Boolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply - * @returns {QuickReplySet} the altered quick reply set + * @returns {Promise} the altered quick reply set */ - updateSet(name, { + async updateSet(name, { disableSend, placeBeforeInput, injectInput, @@ -362,8 +375,22 @@ export class QuickReplyApi { set.disableSend = disableSend ?? false; set.placeBeforeInput = placeBeforeInput ?? false; set.injectInput = injectInput ?? false; - set.save(); - //TODO settings UI must be updated + await set.save(); + this.settingsUi.rerender(); return set; } + + /** + * Delete an existing quick reply set. + * + * @param {String} name name of the existing quick reply set + */ + async deleteSet(name) { + const set = this.getSetByName(name); + if (!set) { + throw new Error(`No quick reply set with name "${name}" found.`); + } + await set.delete(); + this.settingsUi.rerender(); + } } diff --git a/public/scripts/extensions/quick-reply/index.js b/public/scripts/extensions/quick-reply/index.js index edefc4490..04818590e 100644 --- a/public/scripts/extensions/quick-reply/index.js +++ b/public/scripts/extensions/quick-reply/index.js @@ -21,6 +21,32 @@ import { SettingsUi } from './src/ui/SettingsUi.js'; const _VERBOSE = true; export const log = (...msg) => _VERBOSE ? console.log('[QR2]', ...msg) : null; export const warn = (...msg) => _VERBOSE ? console.warn('[QR2]', ...msg) : null; +/** + * Creates a debounced function that delays invoking func until after wait milliseconds have elapsed since the last time the debounced function was invoked. + * @param {Function} func The function to debounce. + * @param {Number} [timeout=300] The timeout in milliseconds. + * @returns {Function} The debounced function. + */ +export function debounceAsync(func, timeout = 300) { + let timer; + /**@type {Promise}*/ + let debouncePromise; + /**@type {Function}*/ + let debounceResolver; + return (...args) => { + clearTimeout(timer); + if (!debouncePromise) { + debouncePromise = new Promise(resolve => { + debounceResolver = resolve; + }); + } + timer = setTimeout(() => { + debounceResolver(func.apply(this, args)); + debouncePromise = null; + }, timeout); + return debouncePromise; + }; +} const defaultConfig = { @@ -154,7 +180,7 @@ const init = async () => { } } - quickReplyApi = new QuickReplyApi(settings); + quickReplyApi = new QuickReplyApi(settings, manager); const slash = new SlashCommandHandler(quickReplyApi); slash.init(); }; diff --git a/public/scripts/extensions/quick-reply/src/QuickReplySet.js b/public/scripts/extensions/quick-reply/src/QuickReplySet.js index 0d0aff585..a2d045b0b 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReplySet.js +++ b/public/scripts/extensions/quick-reply/src/QuickReplySet.js @@ -1,7 +1,6 @@ import { getRequestHeaders } from '../../../../script.js'; import { executeSlashCommands } from '../../../slash-commands.js'; -import { debounce } from '../../../utils.js'; -import { warn } from '../index.js'; +import { debounceAsync, warn } from '../index.js'; import { QuickReply } from './QuickReply.js'; export class QuickReplySet { @@ -44,7 +43,7 @@ export class QuickReplySet { constructor() { - this.save = debounce(()=>this.performSave(), 200); + this.save = debounceAsync(()=>this.performSave(), 200); } init() { diff --git a/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js b/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js index 3f1ee2d2c..b413527b1 100644 --- a/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js +++ b/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js @@ -49,8 +49,9 @@ export class SlashCommandHandler { before - bool - place QR before user input inject - bool - inject user input automatically (if disabled use {{input}}) `.trim(); - registerSlashCommand('qr-set-create', (args, name)=>this.createSet(name, args), ['qr-presetadd'], `[arguments] (label)\n arguments:\n ${presetArgs} – create a new preset (overrides existing ones), example: /qr-presetadd MyNewPreset`, true, true); - registerSlashCommand('qr-set-update', (args, name)=>this.updateSet(name, args), ['qr-presetupdate'], `[arguments] (label)\n arguments:\n ${presetArgs} – update an existing preset, example: /qr-presetupdate enabled=false MyPreset`, true, true); + registerSlashCommand('qr-set-create', (args, name)=>this.createSet(name, args), ['qr-presetadd'], `[arguments] (name)\n arguments:\n ${presetArgs} – create a new preset (overrides existing ones), example: /qr-set-add MyNewPreset`, true, true); + registerSlashCommand('qr-set-update', (args, name)=>this.updateSet(name, args), ['qr-presetupdate'], `[arguments] (name)\n arguments:\n ${presetArgs} – update an existing preset, example: /qr-set-update enabled=false MyPreset`, true, true); + registerSlashCommand('qr-set-delete', (args, name)=>this.deleteSet(name), ['qr-presetdelete'], `(name)\n arguments:\n ${presetArgs} – delete an existing preset, example: /qr-set-delete MyPreset`, true, true); } @@ -234,5 +235,11 @@ export class SlashCommandHandler { toastr.error(ex.message); } } + deleteSet(name) { + try { + this.api.deleteSet(name ?? ''); + } catch (ex) { + toastr.error(ex.message); + } } } From f90e60783c99e6e2a49f5c966475b31c5e98aedc Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Fri, 22 Dec 2023 12:56:33 +0000 Subject: [PATCH 020/522] remove debugger --- public/scripts/extensions/quick-reply/index.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/public/scripts/extensions/quick-reply/index.js b/public/scripts/extensions/quick-reply/index.js index 04818590e..cbc74c5f2 100644 --- a/public/scripts/extensions/quick-reply/index.js +++ b/public/scripts/extensions/quick-reply/index.js @@ -130,8 +130,6 @@ const loadSettings = async () => { try { settings = QuickReplySettings.from(extension_settings.quickReplyV2); } catch (ex) { - //TODO remove debugger - debugger; settings = QuickReplySettings.from(defaultSettings); } }; From cbceb7d1e802baa11d30c696f413e6406bdd205e Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Fri, 22 Dec 2023 13:00:02 +0000 Subject: [PATCH 021/522] add old settings migration --- public/scripts/extensions/quick-reply/index.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/public/scripts/extensions/quick-reply/index.js b/public/scripts/extensions/quick-reply/index.js index cbc74c5f2..e00eaf8ec 100644 --- a/public/scripts/extensions/quick-reply/index.js +++ b/public/scripts/extensions/quick-reply/index.js @@ -13,11 +13,6 @@ import { SettingsUi } from './src/ui/SettingsUi.js'; -//TODO move advanced QR options into own UI class - - - - const _VERBOSE = true; export const log = (...msg) => _VERBOSE ? console.log('[QR2]', ...msg) : null; export const warn = (...msg) => _VERBOSE ? console.warn('[QR2]', ...msg) : null; @@ -123,9 +118,17 @@ const loadSets = async () => { }; const loadSettings = async () => { - //TODO migrate old settings if (!extension_settings.quickReplyV2) { - extension_settings.quickReplyV2 = defaultSettings; + if (!extension_settings.quickReply) { + extension_settings.quickReplyV2 = defaultSettings; + } else { + extension_settings.quickReplyV2 = { + isEnabled: extension_settings.quickReply.quickReplyEnabled ?? false, + isCombined: false, + isPopout: false, + config: extension_settings.quickReply.selectedPreset ?? extension_settings.quickReply.name ?? 'Default', + }; + } } try { settings = QuickReplySettings.from(extension_settings.quickReplyV2); From 82a4ddbe01ed7a7592ee7b810ca384b8c67f4f01 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Fri, 22 Dec 2023 13:55:37 +0000 Subject: [PATCH 022/522] cleanup --- public/scripts/extensions/quick-reply/api/QuickReplyApi.js | 6 +++--- .../scripts/extensions/quick-reply/src/QuickReplySetLink.js | 3 --- .../extensions/quick-reply/src/SlashCommandHandler.js | 1 + public/scripts/extensions/quick-reply/src/ui/ButtonUi.js | 1 + .../extensions/quick-reply/src/ui/ctx/ContextMenu.js | 2 +- 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/public/scripts/extensions/quick-reply/api/QuickReplyApi.js b/public/scripts/extensions/quick-reply/api/QuickReplyApi.js index dc81bd263..938f0dbe1 100644 --- a/public/scripts/extensions/quick-reply/api/QuickReplyApi.js +++ b/public/scripts/extensions/quick-reply/api/QuickReplyApi.js @@ -1,12 +1,12 @@ +// eslint-disable-next-line no-unused-vars import { QuickReply } from '../src/QuickReply.js'; import { QuickReplyContextLink } from '../src/QuickReplyContextLink.js'; import { QuickReplySet } from '../src/QuickReplySet.js'; +// eslint-disable-next-line no-unused-vars import { QuickReplySettings } from '../src/QuickReplySettings.js'; +// eslint-disable-next-line no-unused-vars import { SettingsUi } from '../src/ui/SettingsUi.js'; - - - export class QuickReplyApi { /**@type {QuickReplySettings}*/ settings; /**@type {SettingsUi}*/ settingsUi; diff --git a/public/scripts/extensions/quick-reply/src/QuickReplySetLink.js b/public/scripts/extensions/quick-reply/src/QuickReplySetLink.js index 0c8ec2994..784e9c0fc 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReplySetLink.js +++ b/public/scripts/extensions/quick-reply/src/QuickReplySetLink.js @@ -1,8 +1,5 @@ import { QuickReplySet } from './QuickReplySet.js'; - - - export class QuickReplySetLink { static from(props) { props.set = QuickReplySet.get(props.set); diff --git a/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js b/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js index b413527b1..78a09d6f1 100644 --- a/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js +++ b/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js @@ -1,4 +1,5 @@ import { registerSlashCommand } from '../../../slash-commands.js'; +// eslint-disable-next-line no-unused-vars import { QuickReplyApi } from '../api/QuickReplyApi.js'; export class SlashCommandHandler { diff --git a/public/scripts/extensions/quick-reply/src/ui/ButtonUi.js b/public/scripts/extensions/quick-reply/src/ui/ButtonUi.js index 3acb1f336..a7d910aaf 100644 --- a/public/scripts/extensions/quick-reply/src/ui/ButtonUi.js +++ b/public/scripts/extensions/quick-reply/src/ui/ButtonUi.js @@ -1,6 +1,7 @@ import { animation_duration } from '../../../../../script.js'; import { dragElement } from '../../../../RossAscends-mods.js'; import { loadMovingUIState } from '../../../../power-user.js'; +// eslint-disable-next-line no-unused-vars import { QuickReplySettings } from '../QuickReplySettings.js'; export class ButtonUi { diff --git a/public/scripts/extensions/quick-reply/src/ui/ctx/ContextMenu.js b/public/scripts/extensions/quick-reply/src/ui/ctx/ContextMenu.js index f651ec7e9..fa695e672 100644 --- a/public/scripts/extensions/quick-reply/src/ui/ctx/ContextMenu.js +++ b/public/scripts/extensions/quick-reply/src/ui/ctx/ContextMenu.js @@ -1,5 +1,5 @@ import { QuickReply } from '../../QuickReply.js'; -import { QuickReplyContextLink } from '../../QuickReplyContextLink.js'; +// eslint-disable-next-line no-unused-vars import { QuickReplySet } from '../../QuickReplySet.js'; import { MenuHeader } from './MenuHeader.js'; import { MenuItem } from './MenuItem.js'; From ee06a488b0d836057f004f98c61082f0a8357a7e Mon Sep 17 00:00:00 2001 From: DonMoralez Date: Fri, 22 Dec 2023 17:04:58 +0200 Subject: [PATCH 023/522] Add exclude prefixes checkbox, modified sequence checker --- public/index.html | 9 ++++++++ public/scripts/openai.js | 12 +++++++++++ src/endpoints/backends/chat-completions.js | 25 +++++++++++++--------- src/endpoints/prompt-converters.js | 20 +++++++++++++---- 4 files changed, 52 insertions(+), 14 deletions(-) diff --git a/public/index.html b/public/index.html index 0861c7f2f..df628f01d 100644 --- a/public/index.html +++ b/public/index.html @@ -1540,6 +1540,15 @@
+ +
+ + Exclude Human/Assistant prefixes from being added to the prompt. (Requires 'Add character names' checked). + +
- Exclude Human/Assistant prefixes from being added to the prompt. (Requires 'Add character names' checked). + Exclude Human/Assistant prefixes from being added to the prompt. + (Exeptions: very first/last prompt in prompts, Assistant suffix, Sysprompt human message). + (Requires: 'Add character names' checked, advanced prompting skill.).
+ + +

Testing

+ + +
diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index b6949b0a1..4ea6610a8 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -345,6 +345,32 @@ export class QuickReply { this.updateContext(); }); + + /**@type {HTMLElement}*/ + const executeErrors = dom.querySelector('#qr--modal-executeErrors'); + /**@type {HTMLInputElement}*/ + const executeHide = dom.querySelector('#qr--modal-executeHide'); + let executePromise; + /**@type {HTMLElement}*/ + const executeBtn = dom.querySelector('#qr--modal-execute'); + executeBtn.addEventListener('click', async()=>{ + if (executePromise) return; + executeBtn.classList.add('qr--busy'); + executeErrors.innerHTML = ''; + if (executeHide.checked) { + document.querySelector('#shadow_popup').classList.add('qr--hide'); + } + try { + executePromise = this.onExecute(); + await executePromise; + } catch (ex) { + executeErrors.textContent = ex.message; + } + executePromise = null; + executeBtn.classList.remove('qr--busy'); + document.querySelector('#shadow_popup').classList.remove('qr--hide'); + }); + await popupResult; } else { warn('failed to fetch qrEditor template'); diff --git a/public/scripts/extensions/quick-reply/style.css b/public/scripts/extensions/quick-reply/style.css index 7c32934f9..cd117ba1f 100644 --- a/public/scripts/extensions/quick-reply/style.css +++ b/public/scripts/extensions/quick-reply/style.css @@ -255,3 +255,15 @@ #dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-message { flex: 1 1 auto; } +#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor #qr--modal-execute { + display: flex; + flex-direction: row; + gap: 0.5em; +} +#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor #qr--modal-execute.qr--busy { + opacity: 0.5; + cursor: wait; +} +#shadow_popup.qr--hide { + opacity: 0 !important; +} diff --git a/public/scripts/extensions/quick-reply/style.less b/public/scripts/extensions/quick-reply/style.less index b2e74005a..92581026b 100644 --- a/public/scripts/extensions/quick-reply/style.less +++ b/public/scripts/extensions/quick-reply/style.less @@ -281,6 +281,20 @@ } } } + + #qr--modal-execute { + display: flex; + flex-direction: row; + gap: 0.5em; + &.qr--busy { + opacity: 0.5; + cursor: wait; + } + } } } } + +#shadow_popup.qr--hide { + opacity: 0 !important; +} From f53e051cbf66434c7e111801c1b81f2c42c92dc7 Mon Sep 17 00:00:00 2001 From: valadaptive Date: Sat, 23 Dec 2023 05:24:31 -0500 Subject: [PATCH 033/522] Lift precondition check out of processCommands Instead of passing type and dryRun into processCommands, do the check in Generate, the only function that calls it. This makes the logic clearer. --- public/script.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/public/script.js b/public/script.js index 23be30980..933fbf0ff 100644 --- a/public/script.js +++ b/public/script.js @@ -2354,11 +2354,7 @@ export async function generateQuietPrompt(quiet_prompt, quietToLoud, skipWIAN, q return generateFinished; } -async function processCommands(message, type, dryRun) { - if (dryRun || type == 'regenerate' || type == 'swipe' || type == 'quiet') { - return null; - } - +async function processCommands(message) { const previousText = String($('#send_textarea').val()); const result = await executeSlashCommands(message); @@ -2946,12 +2942,14 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu let message_already_generated = isImpersonate ? `${name1}: ` : `${name2}: `; - const interruptedByCommand = await processCommands($('#send_textarea').val(), type, dryRun); + if (!(dryRun || type == 'regenerate' || type == 'swipe' || type == 'quiet')) { + const interruptedByCommand = await processCommands($('#send_textarea').val()); - if (interruptedByCommand) { - //$("#send_textarea").val('').trigger('input'); - unblockGeneration(); - return Promise.resolve(); + if (interruptedByCommand) { + //$("#send_textarea").val('').trigger('input'); + unblockGeneration(); + return Promise.resolve(); + } } if (main_api == 'kobold' && kai_settings.streaming_kobold && !kai_flags.can_use_streaming) { From d2f86323683fb33195bf34a3aa2da37dfee4f637 Mon Sep 17 00:00:00 2001 From: valadaptive Date: Sat, 23 Dec 2023 05:55:44 -0500 Subject: [PATCH 034/522] Remove populateLegacyTokenCounts Unused and the documentation says it should probably be removed --- public/scripts/PromptManager.js | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/public/scripts/PromptManager.js b/public/scripts/PromptManager.js index 094fe49cc..4e8fb84a8 100644 --- a/public/scripts/PromptManager.js +++ b/public/scripts/PromptManager.js @@ -1293,34 +1293,6 @@ class PromptManager { this.log('Updated token usage with ' + this.tokenUsage); } - /** - * Populates legacy token counts - * - * @deprecated This might serve no purpose and should be evaluated for removal - * - * @param {MessageCollection} messages - */ - populateLegacyTokenCounts(messages) { - // Update general token counts - const chatHistory = messages.getItemByIdentifier('chatHistory'); - const startChat = chatHistory?.getCollection()[0]?.getTokens() || 0; - const continueNudge = chatHistory?.getCollection().find(message => message.identifier === 'continueNudge')?.getTokens() || 0; - - this.tokenHandler.counts = { - ...this.tokenHandler.counts, - ...{ - 'start_chat': startChat, - 'prompt': 0, - 'bias': this.tokenHandler.counts.bias ?? 0, - 'nudge': continueNudge, - 'jailbreak': this.tokenHandler.counts.jailbreak ?? 0, - 'impersonate': 0, - 'examples': this.tokenHandler.counts.dialogueExamples ?? 0, - 'conversation': this.tokenHandler.counts.chatHistory ?? 0, - }, - }; - } - /** * Empties, then re-assembles the container containing the prompt list. */ From 0d3505c44b91783ea675d7ec6e06c86d1ae333be Mon Sep 17 00:00:00 2001 From: valadaptive Date: Sat, 23 Dec 2023 05:58:41 -0500 Subject: [PATCH 035/522] Remove OAI_BEFORE_CHATCOMPLETION Not used in any internal code or extensions I can find. --- public/script.js | 1 - public/scripts/openai.js | 3 --- 2 files changed, 4 deletions(-) diff --git a/public/script.js b/public/script.js index 933fbf0ff..06f362b82 100644 --- a/public/script.js +++ b/public/script.js @@ -320,7 +320,6 @@ export const event_types = { SETTINGS_LOADED_AFTER: 'settings_loaded_after', CHATCOMPLETION_SOURCE_CHANGED: 'chatcompletion_source_changed', CHATCOMPLETION_MODEL_CHANGED: 'chatcompletion_model_changed', - OAI_BEFORE_CHATCOMPLETION: 'oai_before_chatcompletion', OAI_PRESET_CHANGED_BEFORE: 'oai_preset_changed_before', OAI_PRESET_CHANGED_AFTER: 'oai_preset_changed_after', WORLDINFO_SETTINGS_UPDATED: 'worldinfo_settings_updated', diff --git a/public/scripts/openai.js b/public/scripts/openai.js index ffb4a9efe..7126c380f 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -1031,9 +1031,6 @@ function preparePromptsForChatCompletion({ Scenario, charPersonality, name2, wor prompts.set(jbReplacement, prompts.index('jailbreak')); } - // Allow subscribers to manipulate the prompts object - eventSource.emit(event_types.OAI_BEFORE_CHATCOMPLETION, prompts); - return prompts; } From 4fc2f15448fac2e6e8c15546958a9dcc39932bd7 Mon Sep 17 00:00:00 2001 From: valadaptive Date: Sat, 23 Dec 2023 07:54:44 -0500 Subject: [PATCH 036/522] Reformat up Generate() group logic The first two conditions in the group if/else blocks are the same, so we can combine them. --- public/script.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/public/script.js b/public/script.js index 06f362b82..78454a016 100644 --- a/public/script.js +++ b/public/script.js @@ -2978,10 +2978,12 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu chat_metadata['tainted'] = true; } - if (selected_group && !is_group_generating && !dryRun) { - // Returns the promise that generateGroupWrapper returns; resolves when generation is done - return generateGroupWrapper(false, type, { quiet_prompt, force_chid, signal: abortController.signal, quietImage, maxLoops }); - } else if (selected_group && !is_group_generating && dryRun) { + if (selected_group && !is_group_generating) { + if (!dryRun) { + // Returns the promise that generateGroupWrapper returns; resolves when generation is done + return generateGroupWrapper(false, type, { quiet_prompt, force_chid, signal: abortController.signal, quietImage, maxLoops }); + } + const characterIndexMap = new Map(characters.map((char, index) => [char.avatar, index])); const group = groups.find((x) => x.id === selected_group); From 1029ad90a2a858afffba56ce333ebfec17ee4f2c Mon Sep 17 00:00:00 2001 From: valadaptive Date: Mon, 25 Dec 2023 03:19:08 -0500 Subject: [PATCH 037/522] Extract "not in a chat" check into guard clause This lets us remove a layer of indentation, and reveal the error handling logic that was previously hidden below a really long block of code. --- public/script.js | 1874 +++++++++++++++++++++++----------------------- 1 file changed, 939 insertions(+), 935 deletions(-) diff --git a/public/script.js b/public/script.js index 78454a016..4cdd68979 100644 --- a/public/script.js +++ b/public/script.js @@ -3015,946 +3015,950 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu quiet_prompt = main_api == 'novel' && !quietToLoud ? adjustNovelInstructionPrompt(quiet_prompt) : quiet_prompt; } - if (true === dryRun || - (online_status != 'no_connection' && this_chid != undefined && this_chid !== 'invalid-safety-id')) { - let textareaText; - if (type !== 'regenerate' && type !== 'swipe' && type !== 'quiet' && !isImpersonate && !dryRun) { - is_send_press = true; - textareaText = String($('#send_textarea').val()); - $('#send_textarea').val('').trigger('input'); - } else { - textareaText = ''; - if (chat.length && chat[chat.length - 1]['is_user']) { - //do nothing? why does this check exist? - } - else if (type !== 'quiet' && type !== 'swipe' && !isImpersonate && !dryRun && chat.length) { - chat.length = chat.length - 1; - count_view_mes -= 1; - $('#chat').children().last().hide(250, function () { - $(this).remove(); - }); - await eventSource.emit(event_types.MESSAGE_DELETED, chat.length); - } - } + const isChatValid = online_status != 'no_connection' && this_chid != undefined && this_chid !== 'invalid-safety-id'; - if (!type && !textareaText && power_user.continue_on_send && !selected_group && chat.length && !chat[chat.length - 1]['is_user'] && !chat[chat.length - 1]['is_system']) { - type = 'continue'; - } - - const isContinue = type == 'continue'; - - // Rewrite the generation timer to account for the time passed for all the continuations. - if (isContinue && chat.length) { - const prevFinished = chat[chat.length - 1]['gen_finished']; - const prevStarted = chat[chat.length - 1]['gen_started']; - - if (prevFinished && prevStarted) { - const timePassed = prevFinished - prevStarted; - generation_started = new Date(Date.now() - timePassed); - chat[chat.length - 1]['gen_started'] = generation_started; - } - } - - if (!dryRun) { - deactivateSendButtons(); - } - - let { messageBias, promptBias, isUserPromptBias } = getBiasStrings(textareaText, type); - - //********************************* - //PRE FORMATING STRING - //********************************* - - //for normal messages sent from user.. - if ((textareaText != '' || hasPendingFileAttachment()) && !automatic_trigger && type !== 'quiet' && !dryRun) { - // If user message contains no text other than bias - send as a system message - if (messageBias && !removeMacros(textareaText)) { - sendSystemMessage(system_message_types.GENERIC, ' ', { bias: messageBias }); - } - else { - await sendMessageAsUser(textareaText, messageBias); - } - } - else if (textareaText == '' && !automatic_trigger && !dryRun && type === undefined && main_api == 'openai' && oai_settings.send_if_empty.trim().length > 0) { - // Use send_if_empty if set and the user message is empty. Only when sending messages normally - await sendMessageAsUser(oai_settings.send_if_empty.trim(), messageBias); - } - - let { - description, - personality, - persona, - scenario, - mesExamples, - system, - jailbreak, - } = getCharacterCardFields(); - - if (isInstruct) { - system = power_user.prefer_character_prompt && system ? system : baseChatReplace(power_user.instruct.system_prompt, name1, name2); - system = formatInstructModeSystemPrompt(substituteParams(system, name1, name2, power_user.instruct.system_prompt)); - } - - // Depth prompt (character-specific A/N) - removeDepthPrompts(); - const groupDepthPrompts = getGroupDepthPrompts(selected_group, Number(this_chid)); - - if (selected_group && Array.isArray(groupDepthPrompts) && groupDepthPrompts.length > 0) { - groupDepthPrompts.forEach((value, index) => { - setExtensionPrompt('DEPTH_PROMPT_' + index, value.text, extension_prompt_types.IN_CHAT, value.depth, extension_settings.note.allowWIScan); - }); - } else { - const depthPromptText = baseChatReplace(characters[this_chid].data?.extensions?.depth_prompt?.prompt?.trim(), name1, name2) || ''; - const depthPromptDepth = characters[this_chid].data?.extensions?.depth_prompt?.depth ?? depth_prompt_depth_default; - setExtensionPrompt('DEPTH_PROMPT', depthPromptText, extension_prompt_types.IN_CHAT, depthPromptDepth, extension_settings.note.allowWIScan); - } - - // Parse example messages - if (!mesExamples.startsWith('')) { - mesExamples = '\n' + mesExamples.trim(); - } - if (mesExamples.replace(//gi, '').trim().length === 0) { - mesExamples = ''; - } - if (mesExamples && isInstruct) { - mesExamples = formatInstructModeExamples(mesExamples, name1, name2); - } - - const exampleSeparator = power_user.context.example_separator ? `${substituteParams(power_user.context.example_separator)}\n` : ''; - const blockHeading = main_api === 'openai' ? '\n' : exampleSeparator; - let mesExamplesArray = mesExamples.split(//gi).slice(1).map(block => `${blockHeading}${block.trim()}\n`); - - // First message in fresh 1-on-1 chat reacts to user/character settings changes - if (chat.length) { - chat[0].mes = substituteParams(chat[0].mes); - } - - // Collect messages with usable content - let coreChat = chat.filter(x => !x.is_system); - if (type === 'swipe') { - coreChat.pop(); - } - - coreChat = await Promise.all(coreChat.map(async (chatItem, index) => { - let message = chatItem.mes; - let regexType = chatItem.is_user ? regex_placement.USER_INPUT : regex_placement.AI_OUTPUT; - let options = { isPrompt: true }; - - let regexedMessage = getRegexedString(message, regexType, options); - regexedMessage = await appendFileContent(chatItem, regexedMessage); - - return { - ...chatItem, - mes: regexedMessage, - index, - }; - })); - - // Determine token limit - let this_max_context = getMaxContextSize(); - - if (!dryRun && type !== 'quiet') { - console.debug('Running extension interceptors'); - const aborted = await runGenerationInterceptors(coreChat, this_max_context); - - if (aborted) { - console.debug('Generation aborted by extension interceptors'); - unblockGeneration(); - return Promise.resolve(); - } - } else { - console.debug('Skipping extension interceptors for dry run'); - } - - console.log(`Core/all messages: ${coreChat.length}/${chat.length}`); - - // kingbri MARK: - Make sure the prompt bias isn't the same as the user bias - if ((promptBias && !isUserPromptBias) || power_user.always_force_name2 || main_api == 'novel') { - force_name2 = true; - } - - if (isImpersonate) { - force_name2 = false; - } - - ////////////////////////////////// - - let chat2 = []; - let continue_mag = ''; - for (let i = coreChat.length - 1, j = 0; i >= 0; i--, j++) { - // For OpenAI it's only used in WI - if (main_api == 'openai' && (!world_info || world_info.length === 0)) { - console.debug('No WI, skipping chat2 for OAI'); - break; - } - - chat2[i] = formatMessageHistoryItem(coreChat[j], isInstruct, false); - - if (j === 0 && isInstruct) { - // Reformat with the first output sequence (if any) - chat2[i] = formatMessageHistoryItem(coreChat[j], isInstruct, force_output_sequence.FIRST); - } - - // Do not suffix the message for continuation - if (i === 0 && isContinue) { - if (isInstruct) { - // Reformat with the last output sequence (if any) - chat2[i] = formatMessageHistoryItem(coreChat[j], isInstruct, force_output_sequence.LAST); - } - - chat2[i] = chat2[i].slice(0, chat2[i].lastIndexOf(coreChat[j].mes) + coreChat[j].mes.length); - continue_mag = coreChat[j].mes; - } - } - - // Adjust token limit for Horde - let adjustedParams; - if (main_api == 'koboldhorde' && (horde_settings.auto_adjust_context_length || horde_settings.auto_adjust_response_length)) { - try { - adjustedParams = await adjustHordeGenerationParams(max_context, amount_gen); - } - catch { - unblockGeneration(); - return Promise.resolve(); - } - if (horde_settings.auto_adjust_context_length) { - this_max_context = (adjustedParams.maxContextLength - adjustedParams.maxLength); - } - } - - // Extension added strings - // Set non-WI AN - setFloatingPrompt(); - // Add WI to prompt (and also inject WI to AN value via hijack) - - let { worldInfoString, worldInfoBefore, worldInfoAfter, worldInfoDepth } = await getWorldInfoPrompt(chat2, this_max_context); - - if (skipWIAN !== true) { - console.log('skipWIAN not active, adding WIAN'); - // Add all depth WI entries to prompt - flushWIDepthInjections(); - if (Array.isArray(worldInfoDepth)) { - worldInfoDepth.forEach((e) => { - const joinedEntries = e.entries.join('\n'); - setExtensionPrompt(`customDepthWI-${e.depth}`, joinedEntries, extension_prompt_types.IN_CHAT, e.depth); - }); - } - } else { - console.log('skipping WIAN'); - } - - // Add persona description to prompt - addPersonaDescriptionExtensionPrompt(); - // Call combined AN into Generate - let allAnchors = getAllExtensionPrompts(); - const beforeScenarioAnchor = getExtensionPrompt(extension_prompt_types.BEFORE_PROMPT).trimStart(); - const afterScenarioAnchor = getExtensionPrompt(extension_prompt_types.IN_PROMPT); - let zeroDepthAnchor = getExtensionPrompt(extension_prompt_types.IN_CHAT, 0, ' '); - - const storyStringParams = { - description: description, - personality: personality, - persona: persona, - scenario: scenario, - system: isInstruct ? system : '', - char: name2, - user: name1, - wiBefore: worldInfoBefore, - wiAfter: worldInfoAfter, - loreBefore: worldInfoBefore, - loreAfter: worldInfoAfter, - mesExamples: mesExamplesArray.join(''), - }; - - const storyString = renderStoryString(storyStringParams); - - // Story string rendered, safe to remove - if (power_user.strip_examples) { - mesExamplesArray = []; - } - - let oaiMessages = []; - let oaiMessageExamples = []; - - if (main_api === 'openai') { - message_already_generated = ''; - oaiMessages = setOpenAIMessages(coreChat); - oaiMessageExamples = setOpenAIMessageExamples(mesExamplesArray); - } - - // hack for regeneration of the first message - if (chat2.length == 0) { - chat2.push(''); - } - - let examplesString = ''; - let chatString = ''; - let cyclePrompt = ''; - - function getMessagesTokenCount() { - const encodeString = [ - storyString, - examplesString, - chatString, - allAnchors, - quiet_prompt, - cyclePrompt, - ].join('').replace(/\r/gm, ''); - return getTokenCount(encodeString, power_user.token_padding); - } - - // Force pinned examples into the context - let pinExmString; - if (power_user.pin_examples) { - pinExmString = examplesString = mesExamplesArray.join(''); - } - - // Only add the chat in context if past the greeting message - if (isContinue && (chat2.length > 1 || main_api === 'openai')) { - cyclePrompt = chat2.shift(); - } - - // Collect enough messages to fill the context - let arrMes = []; - let tokenCount = getMessagesTokenCount(); - for (let item of chat2) { - // not needed for OAI prompting - if (main_api == 'openai') { - break; - } - - tokenCount += getTokenCount(item.replace(/\r/gm, '')); - chatString = item + chatString; - if (tokenCount < this_max_context) { - arrMes[arrMes.length] = item; - } else { - break; - } - - // Prevent UI thread lock on tokenization - await delay(1); - } - - if (main_api !== 'openai') { - setInContextMessages(arrMes.length, type); - } - - // Estimate how many unpinned example messages fit in the context - tokenCount = getMessagesTokenCount(); - let count_exm_add = 0; - if (!power_user.pin_examples) { - for (let example of mesExamplesArray) { - tokenCount += getTokenCount(example.replace(/\r/gm, '')); - examplesString += example; - if (tokenCount < this_max_context) { - count_exm_add++; - } else { - break; - } - await delay(1); - } - } - - let mesSend = []; - console.debug('calling runGenerate'); - - if (isContinue) { - // Coping mechanism for OAI spacing - const isForceInstruct = isOpenRouterWithInstruct(); - if (main_api === 'openai' && !isForceInstruct && !cyclePrompt.endsWith(' ')) { - cyclePrompt += ' '; - continue_mag += ' '; - } - message_already_generated = continue_mag; - } - - const originalType = type; - - if (!dryRun) { - is_send_press = true; - } - - generatedPromptCache += cyclePrompt; - if (generatedPromptCache.length == 0 || type === 'continue') { - console.debug('generating prompt'); - chatString = ''; - arrMes = arrMes.reverse(); - arrMes.forEach(function (item, i, arr) {// For added anchors and others - // OAI doesn't need all of this - if (main_api === 'openai') { - return; - } - - // Cohee: I'm not even sure what this is for anymore - if (i === arrMes.length - 1 && type !== 'continue') { - item = item.replace(/\n?$/, ''); - } - - mesSend[mesSend.length] = { message: item, extensionPrompts: [] }; - }); - } - - let mesExmString = ''; - - function setPromptString() { - if (main_api == 'openai') { - return; - } - - console.debug('--setting Prompt string'); - mesExmString = pinExmString ?? mesExamplesArray.slice(0, count_exm_add).join(''); - - if (mesSend.length) { - mesSend[mesSend.length - 1].message = modifyLastPromptLine(mesSend[mesSend.length - 1].message); - } - } - - function modifyLastPromptLine(lastMesString) { - //#########QUIET PROMPT STUFF PT2############## - - // Add quiet generation prompt at depth 0 - if (quiet_prompt && quiet_prompt.length) { - - // here name1 is forced for all quiet prompts..why? - const name = name1; - //checks if we are in instruct, if so, formats the chat as such, otherwise just adds the quiet prompt - const quietAppend = isInstruct ? formatInstructModeChat(name, quiet_prompt, false, true, '', name1, name2, false) : `\n${quiet_prompt}`; - - //This begins to fix quietPrompts (particularly /sysgen) for instruct - //previously instruct input sequence was being appended to the last chat message w/o '\n' - //and no output sequence was added after the input's content. - //TODO: respect output_sequence vs last_output_sequence settings - //TODO: decide how to prompt this to clarify who is talking 'Narrator', 'System', etc. - if (isInstruct) { - lastMesString += '\n' + quietAppend; // + power_user.instruct.output_sequence + '\n'; - } else { - lastMesString += quietAppend; - } - - - // Ross: bailing out early prevents quiet prompts from respecting other instruct prompt toggles - // for sysgen, SD, and summary this is desireable as it prevents the AI from responding as char.. - // but for idle prompting, we want the flexibility of the other prompt toggles, and to respect them as per settings in the extension - // need a detection for what the quiet prompt is being asked for... - - // Bail out early? - if (quietToLoud !== true) { - return lastMesString; - } - } - - - // Get instruct mode line - if (isInstruct && !isContinue) { - const name = isImpersonate ? name1 : name2; - lastMesString += formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2); - } - - // Get non-instruct impersonation line - if (!isInstruct && isImpersonate && !isContinue) { - const name = name1; - if (!lastMesString.endsWith('\n')) { - lastMesString += '\n'; - } - lastMesString += name + ':'; - } - - // Add character's name - // Force name append on continue (if not continuing on user message) - if (!isInstruct && force_name2) { - if (!lastMesString.endsWith('\n')) { - lastMesString += '\n'; - } - if (!isContinue || !(chat[chat.length - 1]?.is_user)) { - lastMesString += `${name2}:`; - } - } - - return lastMesString; - } - - // Clean up the already generated prompt for seamless addition - function cleanupPromptCache(promptCache) { - // Remove the first occurrance of character's name - if (promptCache.trimStart().startsWith(`${name2}:`)) { - promptCache = promptCache.replace(`${name2}:`, '').trimStart(); - } - - // Remove the first occurrance of prompt bias - if (promptCache.trimStart().startsWith(promptBias)) { - promptCache = promptCache.replace(promptBias, ''); - } - - // Add a space if prompt cache doesn't start with one - if (!/^\s/.test(promptCache) && !isInstruct && !isContinue) { - promptCache = ' ' + promptCache; - } - - return promptCache; - } - - function checkPromptSize() { - console.debug('---checking Prompt size'); - setPromptString(); - const prompt = [ - storyString, - mesExmString, - mesSend.join(''), - generatedPromptCache, - allAnchors, - quiet_prompt, - ].join('').replace(/\r/gm, ''); - let thisPromptContextSize = getTokenCount(prompt, power_user.token_padding); - - if (thisPromptContextSize > this_max_context) { //if the prepared prompt is larger than the max context size... - if (count_exm_add > 0) { // ..and we have example mesages.. - count_exm_add--; // remove the example messages... - checkPromptSize(); // and try agin... - } else if (mesSend.length > 0) { // if the chat history is longer than 0 - mesSend.shift(); // remove the first (oldest) chat entry.. - checkPromptSize(); // and check size again.. - } else { - //end - console.debug(`---mesSend.length = ${mesSend.length}`); - } - } - } - - if (generatedPromptCache.length > 0 && main_api !== 'openai') { - console.debug('---Generated Prompt Cache length: ' + generatedPromptCache.length); - checkPromptSize(); - } else { - console.debug('---calling setPromptString ' + generatedPromptCache.length); - setPromptString(); - } - - // Fetches the combined prompt for both negative and positive prompts - const cfgGuidanceScale = getGuidanceScale(); - - // For prompt bit itemization - let mesSendString = ''; - - function getCombinedPrompt(isNegative) { - // Only return if the guidance scale doesn't exist or the value is 1 - // Also don't return if constructing the neutral prompt - if (isNegative && (!cfgGuidanceScale || cfgGuidanceScale?.value === 1)) { - return; - } - - // OAI has its own prompt manager. No need to do anything here - if (main_api === 'openai') { - return ''; - } - - // Deep clone - let finalMesSend = structuredClone(mesSend); - - // TODO: Rewrite getExtensionPrompt to not require multiple for loops - // Set all extension prompts where insertion depth > mesSend length - if (finalMesSend.length) { - for (let upperDepth = MAX_INJECTION_DEPTH; upperDepth >= finalMesSend.length; upperDepth--) { - const upperAnchor = getExtensionPrompt(extension_prompt_types.IN_CHAT, upperDepth); - if (upperAnchor && upperAnchor.length) { - finalMesSend[0].extensionPrompts.push(upperAnchor); - } - } - } - - finalMesSend.forEach((mesItem, index) => { - if (index === 0) { - return; - } - - const anchorDepth = Math.abs(index - finalMesSend.length); - // NOTE: Depth injected here! - const extensionAnchor = getExtensionPrompt(extension_prompt_types.IN_CHAT, anchorDepth); - - if (anchorDepth >= 0 && extensionAnchor && extensionAnchor.length) { - mesItem.extensionPrompts.push(extensionAnchor); - } - }); - - // TODO: Move zero-depth anchor append to work like CFG and bias appends - if (zeroDepthAnchor?.length && !isContinue) { - console.debug(/\s/.test(finalMesSend[finalMesSend.length - 1].message.slice(-1))); - finalMesSend[finalMesSend.length - 1].message += - /\s/.test(finalMesSend[finalMesSend.length - 1].message.slice(-1)) - ? zeroDepthAnchor - : `${zeroDepthAnchor}`; - } - - let cfgPrompt = {}; - if (cfgGuidanceScale && cfgGuidanceScale?.value !== 1) { - cfgPrompt = getCfgPrompt(cfgGuidanceScale, isNegative); - } - - if (cfgPrompt && cfgPrompt?.value) { - if (cfgPrompt?.depth === 0) { - finalMesSend[finalMesSend.length - 1].message += - /\s/.test(finalMesSend[finalMesSend.length - 1].message.slice(-1)) - ? cfgPrompt.value - : ` ${cfgPrompt.value}`; - } else { - // TODO: Make all extension prompts use an array/splice method - const lengthDiff = mesSend.length - cfgPrompt.depth; - const cfgDepth = lengthDiff >= 0 ? lengthDiff : 0; - finalMesSend[cfgDepth].extensionPrompts.push(`${cfgPrompt.value}\n`); - } - } - - // Add prompt bias after everything else - // Always run with continue - if (!isInstruct && !isImpersonate) { - if (promptBias.trim().length !== 0) { - finalMesSend[finalMesSend.length - 1].message += - /\s/.test(finalMesSend[finalMesSend.length - 1].message.slice(-1)) - ? promptBias.trimStart() - : ` ${promptBias.trimStart()}`; - } - } - - // Prune from prompt cache if it exists - if (generatedPromptCache.length !== 0) { - generatedPromptCache = cleanupPromptCache(generatedPromptCache); - } - - // Flattens the multiple prompt objects to a string. - const combine = () => { - // Right now, everything is suffixed with a newline - mesSendString = finalMesSend.map((e) => `${e.extensionPrompts.join('')}${e.message}`).join(''); - - // add a custom dingus (if defined) - mesSendString = addChatsSeparator(mesSendString); - - // add chat preamble - mesSendString = addChatsPreamble(mesSendString); - - let combinedPrompt = beforeScenarioAnchor + - storyString + - afterScenarioAnchor + - mesExmString + - mesSendString + - generatedPromptCache; - - combinedPrompt = combinedPrompt.replace(/\r/gm, ''); - - if (power_user.collapse_newlines) { - combinedPrompt = collapseNewlines(combinedPrompt); - } - - return combinedPrompt; - }; - - let data = { - api: main_api, - combinedPrompt: null, - description, - personality, - persona, - scenario, - char: name2, - user: name1, - beforeScenarioAnchor, - afterScenarioAnchor, - mesExmString, - finalMesSend, - generatedPromptCache, - main: system, - jailbreak, - naiPreamble: nai_settings.preamble, - }; - - // Before returning the combined prompt, give available context related information to all subscribers. - eventSource.emitAndWait(event_types.GENERATE_BEFORE_COMBINE_PROMPTS, data); - - // If one or multiple subscribers return a value, forfeit the responsibillity of flattening the context. - return !data.combinedPrompt ? combine() : data.combinedPrompt; - } - - // Get the negative prompt first since it has the unmodified mesSend array - let negativePrompt = main_api == 'textgenerationwebui' ? getCombinedPrompt(true) : undefined; - let finalPrompt = getCombinedPrompt(false); - - // Include the entire guidance scale object - const cfgValues = cfgGuidanceScale && cfgGuidanceScale?.value !== 1 ? ({ guidanceScale: cfgGuidanceScale, negativePrompt: negativePrompt }) : null; - - let maxLength = Number(amount_gen); // how many tokens the AI will be requested to generate - let thisPromptBits = []; - - // TODO: Make this a switch - if (main_api == 'koboldhorde' && horde_settings.auto_adjust_response_length) { - maxLength = Math.min(maxLength, adjustedParams.maxLength); - maxLength = Math.max(maxLength, MIN_LENGTH); // prevent validation errors - } - - let generate_data; - if (main_api == 'koboldhorde' || main_api == 'kobold') { - generate_data = { - prompt: finalPrompt, - gui_settings: true, - max_length: maxLength, - max_context_length: max_context, - }; - - if (preset_settings != 'gui') { - const isHorde = main_api == 'koboldhorde'; - const presetSettings = koboldai_settings[koboldai_setting_names[preset_settings]]; - const maxContext = (adjustedParams && horde_settings.auto_adjust_context_length) ? adjustedParams.maxContextLength : max_context; - generate_data = getKoboldGenerationData(finalPrompt, presetSettings, maxLength, maxContext, isHorde, type); - } - } - else if (main_api == 'textgenerationwebui') { - generate_data = getTextGenGenerationData(finalPrompt, maxLength, isImpersonate, isContinue, cfgValues, type); - } - else if (main_api == 'novel') { - const presetSettings = novelai_settings[novelai_setting_names[nai_settings.preset_settings_novel]]; - generate_data = getNovelGenerationData(finalPrompt, presetSettings, maxLength, isImpersonate, isContinue, cfgValues, type); - } - else if (main_api == 'openai') { - let [prompt, counts] = await prepareOpenAIMessages({ - name2: name2, - charDescription: description, - charPersonality: personality, - Scenario: scenario, - worldInfoBefore: worldInfoBefore, - worldInfoAfter: worldInfoAfter, - extensionPrompts: extension_prompts, - bias: promptBias, - type: type, - quietPrompt: quiet_prompt, - quietImage: quietImage, - cyclePrompt: cyclePrompt, - systemPromptOverride: system, - jailbreakPromptOverride: jailbreak, - personaDescription: persona, - messages: oaiMessages, - messageExamples: oaiMessageExamples, - }, dryRun); - generate_data = { prompt: prompt }; - - // counts will return false if the user has not enabled the token breakdown feature - if (counts) { - parseTokenCounts(counts, thisPromptBits); - } - - if (!dryRun) { - setInContextMessages(openai_messages_count, type); - } - } - - async function finishGenerating() { - if (dryRun) return { error: 'dryRun' }; - - if (power_user.console_log_prompts) { - console.log(generate_data.prompt); - } - - console.debug('rungenerate calling API'); - - showStopButton(); - - //set array object for prompt token itemization of this message - let currentArrayEntry = Number(thisPromptBits.length - 1); - let additionalPromptStuff = { - ...thisPromptBits[currentArrayEntry], - rawPrompt: generate_data.prompt || generate_data.input, - mesId: getNextMessageId(type), - allAnchors: allAnchors, - summarizeString: (extension_prompts['1_memory']?.value || ''), - authorsNoteString: (extension_prompts['2_floating_prompt']?.value || ''), - smartContextString: (extension_prompts['chromadb']?.value || ''), - worldInfoString: worldInfoString, - storyString: storyString, - beforeScenarioAnchor: beforeScenarioAnchor, - afterScenarioAnchor: afterScenarioAnchor, - examplesString: examplesString, - mesSendString: mesSendString, - generatedPromptCache: generatedPromptCache, - promptBias: promptBias, - finalPrompt: finalPrompt, - charDescription: description, - charPersonality: personality, - scenarioText: scenario, - this_max_context: this_max_context, - padding: power_user.token_padding, - main_api: main_api, - instruction: isInstruct ? substituteParams(power_user.prefer_character_prompt && system ? system : power_user.instruct.system_prompt) : '', - userPersona: (power_user.persona_description || ''), - }; - - thisPromptBits = additionalPromptStuff; - - //console.log(thisPromptBits); - const itemizedIndex = itemizedPrompts.findIndex((item) => item.mesId === thisPromptBits['mesId']); - - if (itemizedIndex !== -1) { - itemizedPrompts[itemizedIndex] = thisPromptBits; - } - else { - itemizedPrompts.push(thisPromptBits); - } - - console.debug(`pushed prompt bits to itemizedPrompts array. Length is now: ${itemizedPrompts.length}`); - - if (isStreamingEnabled() && type !== 'quiet') { - streamingProcessor = new StreamingProcessor(type, force_name2, generation_started, message_already_generated); - if (isContinue) { - // Save reply does add cycle text to the prompt, so it's not needed here - streamingProcessor.firstMessageText = ''; - } - - streamingProcessor.generator = await sendStreamingRequest(type, generate_data); - - hideSwipeButtons(); - let getMessage = await streamingProcessor.generate(); - let messageChunk = cleanUpMessage(getMessage, isImpersonate, isContinue, false); - - if (isContinue) { - getMessage = continue_mag + getMessage; - } - - if (streamingProcessor && !streamingProcessor.isStopped && streamingProcessor.isFinished) { - await streamingProcessor.onFinishStreaming(streamingProcessor.messageId, getMessage); - streamingProcessor = null; - triggerAutoContinue(messageChunk, isImpersonate); - } - } else { - return await sendGenerationRequest(type, generate_data); - } - } - - return finishGenerating().then(onSuccess, onError); - - async function onSuccess(data) { - if (!data) return; - let messageChunk = ''; - - if (data.error == 'dryRun') { - generatedPromptCache = ''; - return; - } - - if (!data.error) { - //const getData = await response.json(); - let getMessage = extractMessageFromData(data); - let title = extractTitleFromData(data); - kobold_horde_model = title; - - const swipes = extractMultiSwipes(data, type); - - messageChunk = cleanUpMessage(getMessage, isImpersonate, isContinue, false); - - if (isContinue) { - getMessage = continue_mag + getMessage; - } - - //Formating - const displayIncomplete = type === 'quiet' && !quietToLoud; - getMessage = cleanUpMessage(getMessage, isImpersonate, isContinue, displayIncomplete); - - if (getMessage.length > 0) { - if (isImpersonate) { - $('#send_textarea').val(getMessage).trigger('input'); - generatedPromptCache = ''; - await eventSource.emit(event_types.IMPERSONATE_READY, getMessage); - } - else if (type == 'quiet') { - unblockGeneration(); - return getMessage; - } - else { - // Without streaming we'll be having a full message on continuation. Treat it as a last chunk. - if (originalType !== 'continue') { - ({ type, getMessage } = await saveReply(type, getMessage, false, title, swipes)); - } - else { - ({ type, getMessage } = await saveReply('appendFinal', getMessage, false, title, swipes)); - } - } - - if (type !== 'quiet') { - playMessageSound(); - } - } else { - // If maxLoops is not passed in (e.g. first time generating), set it to MAX_GENERATION_LOOPS - maxLoops ??= MAX_GENERATION_LOOPS; - - if (maxLoops === 0) { - if (type !== 'quiet') { - throwCircuitBreakerError(); - } - throw new Error('Generate circuit breaker interruption'); - } - - // regenerate with character speech reenforced - // to make sure we leave on swipe type while also adding the name2 appendage - await delay(1000); - // The first await is for waiting for the generate to start. The second one is waiting for it to finish - const result = await await Generate(type, { automatic_trigger, force_name2: true, quiet_prompt, skipWIAN, force_chid, maxLoops: maxLoops - 1 }); - return result; - } - - if (power_user.auto_swipe) { - console.debug('checking for autoswipeblacklist on non-streaming message'); - function containsBlacklistedWords(getMessage, blacklist, threshold) { - console.debug('checking blacklisted words'); - const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi'); - const matches = getMessage.match(regex) || []; - return matches.length >= threshold; - } - - const generatedTextFiltered = (getMessage) => { - if (power_user.auto_swipe_blacklist_threshold) { - if (containsBlacklistedWords(getMessage, power_user.auto_swipe_blacklist, power_user.auto_swipe_blacklist_threshold)) { - console.debug('Generated text has blacklisted words'); - return true; - } - } - - return false; - }; - if (generatedTextFiltered(getMessage)) { - console.debug('swiping right automatically'); - is_send_press = false; - swipe_right(); - // TODO: do we want to resolve after an auto-swipe? - return; - } - } - } else { - generatedPromptCache = ''; - - if (data?.response) { - toastr.error(data.response, 'API Error'); - } - throw data?.response; - } - - console.debug('/api/chats/save called by /Generate'); - await saveChatConditional(); - unblockGeneration(); - streamingProcessor = null; - - if (type !== 'quiet') { - triggerAutoContinue(messageChunk, isImpersonate); - } - } - - function onError(exception) { - if (typeof exception?.error?.message === 'string') { - toastr.error(exception.error.message, 'Error', { timeOut: 10000, extendedTimeOut: 20000 }); - } - - unblockGeneration(); - console.log(exception); - streamingProcessor = null; - throw exception; - } - } else { //generate's primary loop ends, after this is error handling for no-connection or safety-id + // We can't do anything because we're not in a chat right now. (Unless it's a dry run, in which case we need to + // assemble the prompt so we can count its tokens regardless of whether a chat is active.) + if (!dryRun && !isChatValid) { if (this_chid === undefined || this_chid === 'invalid-safety-id') { toastr.warning('Сharacter is not selected'); } is_send_press = false; + return Promise.resolve(); + } + + let textareaText; + if (type !== 'regenerate' && type !== 'swipe' && type !== 'quiet' && !isImpersonate && !dryRun) { + is_send_press = true; + textareaText = String($('#send_textarea').val()); + $('#send_textarea').val('').trigger('input'); + } else { + textareaText = ''; + if (chat.length && chat[chat.length - 1]['is_user']) { + //do nothing? why does this check exist? + } + else if (type !== 'quiet' && type !== 'swipe' && !isImpersonate && !dryRun && chat.length) { + chat.length = chat.length - 1; + count_view_mes -= 1; + $('#chat').children().last().hide(250, function () { + $(this).remove(); + }); + await eventSource.emit(event_types.MESSAGE_DELETED, chat.length); + } + } + + if (!type && !textareaText && power_user.continue_on_send && !selected_group && chat.length && !chat[chat.length - 1]['is_user'] && !chat[chat.length - 1]['is_system']) { + type = 'continue'; + } + + const isContinue = type == 'continue'; + + // Rewrite the generation timer to account for the time passed for all the continuations. + if (isContinue && chat.length) { + const prevFinished = chat[chat.length - 1]['gen_finished']; + const prevStarted = chat[chat.length - 1]['gen_started']; + + if (prevFinished && prevStarted) { + const timePassed = prevFinished - prevStarted; + generation_started = new Date(Date.now() - timePassed); + chat[chat.length - 1]['gen_started'] = generation_started; + } + } + + if (!dryRun) { + deactivateSendButtons(); + } + + let { messageBias, promptBias, isUserPromptBias } = getBiasStrings(textareaText, type); + + //********************************* + //PRE FORMATING STRING + //********************************* + + //for normal messages sent from user.. + if ((textareaText != '' || hasPendingFileAttachment()) && !automatic_trigger && type !== 'quiet' && !dryRun) { + // If user message contains no text other than bias - send as a system message + if (messageBias && !removeMacros(textareaText)) { + sendSystemMessage(system_message_types.GENERIC, ' ', { bias: messageBias }); + } + else { + await sendMessageAsUser(textareaText, messageBias); + } + } + else if (textareaText == '' && !automatic_trigger && !dryRun && type === undefined && main_api == 'openai' && oai_settings.send_if_empty.trim().length > 0) { + // Use send_if_empty if set and the user message is empty. Only when sending messages normally + await sendMessageAsUser(oai_settings.send_if_empty.trim(), messageBias); + } + + let { + description, + personality, + persona, + scenario, + mesExamples, + system, + jailbreak, + } = getCharacterCardFields(); + + if (isInstruct) { + system = power_user.prefer_character_prompt && system ? system : baseChatReplace(power_user.instruct.system_prompt, name1, name2); + system = formatInstructModeSystemPrompt(substituteParams(system, name1, name2, power_user.instruct.system_prompt)); + } + + // Depth prompt (character-specific A/N) + removeDepthPrompts(); + const groupDepthPrompts = getGroupDepthPrompts(selected_group, Number(this_chid)); + + if (selected_group && Array.isArray(groupDepthPrompts) && groupDepthPrompts.length > 0) { + groupDepthPrompts.forEach((value, index) => { + setExtensionPrompt('DEPTH_PROMPT_' + index, value.text, extension_prompt_types.IN_CHAT, value.depth, extension_settings.note.allowWIScan); + }); + } else { + const depthPromptText = baseChatReplace(characters[this_chid].data?.extensions?.depth_prompt?.prompt?.trim(), name1, name2) || ''; + const depthPromptDepth = characters[this_chid].data?.extensions?.depth_prompt?.depth ?? depth_prompt_depth_default; + setExtensionPrompt('DEPTH_PROMPT', depthPromptText, extension_prompt_types.IN_CHAT, depthPromptDepth, extension_settings.note.allowWIScan); + } + + // Parse example messages + if (!mesExamples.startsWith('')) { + mesExamples = '\n' + mesExamples.trim(); + } + if (mesExamples.replace(//gi, '').trim().length === 0) { + mesExamples = ''; + } + if (mesExamples && isInstruct) { + mesExamples = formatInstructModeExamples(mesExamples, name1, name2); + } + + const exampleSeparator = power_user.context.example_separator ? `${substituteParams(power_user.context.example_separator)}\n` : ''; + const blockHeading = main_api === 'openai' ? '\n' : exampleSeparator; + let mesExamplesArray = mesExamples.split(//gi).slice(1).map(block => `${blockHeading}${block.trim()}\n`); + + // First message in fresh 1-on-1 chat reacts to user/character settings changes + if (chat.length) { + chat[0].mes = substituteParams(chat[0].mes); + } + + // Collect messages with usable content + let coreChat = chat.filter(x => !x.is_system); + if (type === 'swipe') { + coreChat.pop(); + } + + coreChat = await Promise.all(coreChat.map(async (chatItem, index) => { + let message = chatItem.mes; + let regexType = chatItem.is_user ? regex_placement.USER_INPUT : regex_placement.AI_OUTPUT; + let options = { isPrompt: true }; + + let regexedMessage = getRegexedString(message, regexType, options); + regexedMessage = await appendFileContent(chatItem, regexedMessage); + + return { + ...chatItem, + mes: regexedMessage, + index, + }; + })); + + // Determine token limit + let this_max_context = getMaxContextSize(); + + if (!dryRun && type !== 'quiet') { + console.debug('Running extension interceptors'); + const aborted = await runGenerationInterceptors(coreChat, this_max_context); + + if (aborted) { + console.debug('Generation aborted by extension interceptors'); + unblockGeneration(); + return Promise.resolve(); + } + } else { + console.debug('Skipping extension interceptors for dry run'); + } + + console.log(`Core/all messages: ${coreChat.length}/${chat.length}`); + + // kingbri MARK: - Make sure the prompt bias isn't the same as the user bias + if ((promptBias && !isUserPromptBias) || power_user.always_force_name2 || main_api == 'novel') { + force_name2 = true; + } + + if (isImpersonate) { + force_name2 = false; + } + + ////////////////////////////////// + + let chat2 = []; + let continue_mag = ''; + for (let i = coreChat.length - 1, j = 0; i >= 0; i--, j++) { + // For OpenAI it's only used in WI + if (main_api == 'openai' && (!world_info || world_info.length === 0)) { + console.debug('No WI, skipping chat2 for OAI'); + break; + } + + chat2[i] = formatMessageHistoryItem(coreChat[j], isInstruct, false); + + if (j === 0 && isInstruct) { + // Reformat with the first output sequence (if any) + chat2[i] = formatMessageHistoryItem(coreChat[j], isInstruct, force_output_sequence.FIRST); + } + + // Do not suffix the message for continuation + if (i === 0 && isContinue) { + if (isInstruct) { + // Reformat with the last output sequence (if any) + chat2[i] = formatMessageHistoryItem(coreChat[j], isInstruct, force_output_sequence.LAST); + } + + chat2[i] = chat2[i].slice(0, chat2[i].lastIndexOf(coreChat[j].mes) + coreChat[j].mes.length); + continue_mag = coreChat[j].mes; + } + } + + // Adjust token limit for Horde + let adjustedParams; + if (main_api == 'koboldhorde' && (horde_settings.auto_adjust_context_length || horde_settings.auto_adjust_response_length)) { + try { + adjustedParams = await adjustHordeGenerationParams(max_context, amount_gen); + } + catch { + unblockGeneration(); + return Promise.resolve(); + } + if (horde_settings.auto_adjust_context_length) { + this_max_context = (adjustedParams.maxContextLength - adjustedParams.maxLength); + } + } + + // Extension added strings + // Set non-WI AN + setFloatingPrompt(); + // Add WI to prompt (and also inject WI to AN value via hijack) + + let { worldInfoString, worldInfoBefore, worldInfoAfter, worldInfoDepth } = await getWorldInfoPrompt(chat2, this_max_context); + + if (skipWIAN !== true) { + console.log('skipWIAN not active, adding WIAN'); + // Add all depth WI entries to prompt + flushWIDepthInjections(); + if (Array.isArray(worldInfoDepth)) { + worldInfoDepth.forEach((e) => { + const joinedEntries = e.entries.join('\n'); + setExtensionPrompt(`customDepthWI-${e.depth}`, joinedEntries, extension_prompt_types.IN_CHAT, e.depth); + }); + } + } else { + console.log('skipping WIAN'); + } + + // Add persona description to prompt + addPersonaDescriptionExtensionPrompt(); + // Call combined AN into Generate + let allAnchors = getAllExtensionPrompts(); + const beforeScenarioAnchor = getExtensionPrompt(extension_prompt_types.BEFORE_PROMPT).trimStart(); + const afterScenarioAnchor = getExtensionPrompt(extension_prompt_types.IN_PROMPT); + let zeroDepthAnchor = getExtensionPrompt(extension_prompt_types.IN_CHAT, 0, ' '); + + const storyStringParams = { + description: description, + personality: personality, + persona: persona, + scenario: scenario, + system: isInstruct ? system : '', + char: name2, + user: name1, + wiBefore: worldInfoBefore, + wiAfter: worldInfoAfter, + loreBefore: worldInfoBefore, + loreAfter: worldInfoAfter, + mesExamples: mesExamplesArray.join(''), + }; + + const storyString = renderStoryString(storyStringParams); + + // Story string rendered, safe to remove + if (power_user.strip_examples) { + mesExamplesArray = []; + } + + let oaiMessages = []; + let oaiMessageExamples = []; + + if (main_api === 'openai') { + message_already_generated = ''; + oaiMessages = setOpenAIMessages(coreChat); + oaiMessageExamples = setOpenAIMessageExamples(mesExamplesArray); + } + + // hack for regeneration of the first message + if (chat2.length == 0) { + chat2.push(''); + } + + let examplesString = ''; + let chatString = ''; + let cyclePrompt = ''; + + function getMessagesTokenCount() { + const encodeString = [ + storyString, + examplesString, + chatString, + allAnchors, + quiet_prompt, + cyclePrompt, + ].join('').replace(/\r/gm, ''); + return getTokenCount(encodeString, power_user.token_padding); + } + + // Force pinned examples into the context + let pinExmString; + if (power_user.pin_examples) { + pinExmString = examplesString = mesExamplesArray.join(''); + } + + // Only add the chat in context if past the greeting message + if (isContinue && (chat2.length > 1 || main_api === 'openai')) { + cyclePrompt = chat2.shift(); + } + + // Collect enough messages to fill the context + let arrMes = []; + let tokenCount = getMessagesTokenCount(); + for (let item of chat2) { + // not needed for OAI prompting + if (main_api == 'openai') { + break; + } + + tokenCount += getTokenCount(item.replace(/\r/gm, '')); + chatString = item + chatString; + if (tokenCount < this_max_context) { + arrMes[arrMes.length] = item; + } else { + break; + } + + // Prevent UI thread lock on tokenization + await delay(1); + } + + if (main_api !== 'openai') { + setInContextMessages(arrMes.length, type); + } + + // Estimate how many unpinned example messages fit in the context + tokenCount = getMessagesTokenCount(); + let count_exm_add = 0; + if (!power_user.pin_examples) { + for (let example of mesExamplesArray) { + tokenCount += getTokenCount(example.replace(/\r/gm, '')); + examplesString += example; + if (tokenCount < this_max_context) { + count_exm_add++; + } else { + break; + } + await delay(1); + } + } + + let mesSend = []; + console.debug('calling runGenerate'); + + if (isContinue) { + // Coping mechanism for OAI spacing + const isForceInstruct = isOpenRouterWithInstruct(); + if (main_api === 'openai' && !isForceInstruct && !cyclePrompt.endsWith(' ')) { + cyclePrompt += ' '; + continue_mag += ' '; + } + message_already_generated = continue_mag; + } + + const originalType = type; + + if (!dryRun) { + is_send_press = true; + } + + generatedPromptCache += cyclePrompt; + if (generatedPromptCache.length == 0 || type === 'continue') { + console.debug('generating prompt'); + chatString = ''; + arrMes = arrMes.reverse(); + arrMes.forEach(function (item, i, arr) {// For added anchors and others + // OAI doesn't need all of this + if (main_api === 'openai') { + return; + } + + // Cohee: I'm not even sure what this is for anymore + if (i === arrMes.length - 1 && type !== 'continue') { + item = item.replace(/\n?$/, ''); + } + + mesSend[mesSend.length] = { message: item, extensionPrompts: [] }; + }); + } + + let mesExmString = ''; + + function setPromptString() { + if (main_api == 'openai') { + return; + } + + console.debug('--setting Prompt string'); + mesExmString = pinExmString ?? mesExamplesArray.slice(0, count_exm_add).join(''); + + if (mesSend.length) { + mesSend[mesSend.length - 1].message = modifyLastPromptLine(mesSend[mesSend.length - 1].message); + } + } + + function modifyLastPromptLine(lastMesString) { + //#########QUIET PROMPT STUFF PT2############## + + // Add quiet generation prompt at depth 0 + if (quiet_prompt && quiet_prompt.length) { + + // here name1 is forced for all quiet prompts..why? + const name = name1; + //checks if we are in instruct, if so, formats the chat as such, otherwise just adds the quiet prompt + const quietAppend = isInstruct ? formatInstructModeChat(name, quiet_prompt, false, true, '', name1, name2, false) : `\n${quiet_prompt}`; + + //This begins to fix quietPrompts (particularly /sysgen) for instruct + //previously instruct input sequence was being appended to the last chat message w/o '\n' + //and no output sequence was added after the input's content. + //TODO: respect output_sequence vs last_output_sequence settings + //TODO: decide how to prompt this to clarify who is talking 'Narrator', 'System', etc. + if (isInstruct) { + lastMesString += '\n' + quietAppend; // + power_user.instruct.output_sequence + '\n'; + } else { + lastMesString += quietAppend; + } + + + // Ross: bailing out early prevents quiet prompts from respecting other instruct prompt toggles + // for sysgen, SD, and summary this is desireable as it prevents the AI from responding as char.. + // but for idle prompting, we want the flexibility of the other prompt toggles, and to respect them as per settings in the extension + // need a detection for what the quiet prompt is being asked for... + + // Bail out early? + if (quietToLoud !== true) { + return lastMesString; + } + } + + + // Get instruct mode line + if (isInstruct && !isContinue) { + const name = isImpersonate ? name1 : name2; + lastMesString += formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2); + } + + // Get non-instruct impersonation line + if (!isInstruct && isImpersonate && !isContinue) { + const name = name1; + if (!lastMesString.endsWith('\n')) { + lastMesString += '\n'; + } + lastMesString += name + ':'; + } + + // Add character's name + // Force name append on continue (if not continuing on user message) + if (!isInstruct && force_name2) { + if (!lastMesString.endsWith('\n')) { + lastMesString += '\n'; + } + if (!isContinue || !(chat[chat.length - 1]?.is_user)) { + lastMesString += `${name2}:`; + } + } + + return lastMesString; + } + + // Clean up the already generated prompt for seamless addition + function cleanupPromptCache(promptCache) { + // Remove the first occurrance of character's name + if (promptCache.trimStart().startsWith(`${name2}:`)) { + promptCache = promptCache.replace(`${name2}:`, '').trimStart(); + } + + // Remove the first occurrance of prompt bias + if (promptCache.trimStart().startsWith(promptBias)) { + promptCache = promptCache.replace(promptBias, ''); + } + + // Add a space if prompt cache doesn't start with one + if (!/^\s/.test(promptCache) && !isInstruct && !isContinue) { + promptCache = ' ' + promptCache; + } + + return promptCache; + } + + function checkPromptSize() { + console.debug('---checking Prompt size'); + setPromptString(); + const prompt = [ + storyString, + mesExmString, + mesSend.join(''), + generatedPromptCache, + allAnchors, + quiet_prompt, + ].join('').replace(/\r/gm, ''); + let thisPromptContextSize = getTokenCount(prompt, power_user.token_padding); + + if (thisPromptContextSize > this_max_context) { //if the prepared prompt is larger than the max context size... + if (count_exm_add > 0) { // ..and we have example mesages.. + count_exm_add--; // remove the example messages... + checkPromptSize(); // and try agin... + } else if (mesSend.length > 0) { // if the chat history is longer than 0 + mesSend.shift(); // remove the first (oldest) chat entry.. + checkPromptSize(); // and check size again.. + } else { + //end + console.debug(`---mesSend.length = ${mesSend.length}`); + } + } + } + + if (generatedPromptCache.length > 0 && main_api !== 'openai') { + console.debug('---Generated Prompt Cache length: ' + generatedPromptCache.length); + checkPromptSize(); + } else { + console.debug('---calling setPromptString ' + generatedPromptCache.length); + setPromptString(); + } + + // Fetches the combined prompt for both negative and positive prompts + const cfgGuidanceScale = getGuidanceScale(); + + // For prompt bit itemization + let mesSendString = ''; + + function getCombinedPrompt(isNegative) { + // Only return if the guidance scale doesn't exist or the value is 1 + // Also don't return if constructing the neutral prompt + if (isNegative && (!cfgGuidanceScale || cfgGuidanceScale?.value === 1)) { + return; + } + + // OAI has its own prompt manager. No need to do anything here + if (main_api === 'openai') { + return ''; + } + + // Deep clone + let finalMesSend = structuredClone(mesSend); + + // TODO: Rewrite getExtensionPrompt to not require multiple for loops + // Set all extension prompts where insertion depth > mesSend length + if (finalMesSend.length) { + for (let upperDepth = MAX_INJECTION_DEPTH; upperDepth >= finalMesSend.length; upperDepth--) { + const upperAnchor = getExtensionPrompt(extension_prompt_types.IN_CHAT, upperDepth); + if (upperAnchor && upperAnchor.length) { + finalMesSend[0].extensionPrompts.push(upperAnchor); + } + } + } + + finalMesSend.forEach((mesItem, index) => { + if (index === 0) { + return; + } + + const anchorDepth = Math.abs(index - finalMesSend.length); + // NOTE: Depth injected here! + const extensionAnchor = getExtensionPrompt(extension_prompt_types.IN_CHAT, anchorDepth); + + if (anchorDepth >= 0 && extensionAnchor && extensionAnchor.length) { + mesItem.extensionPrompts.push(extensionAnchor); + } + }); + + // TODO: Move zero-depth anchor append to work like CFG and bias appends + if (zeroDepthAnchor?.length && !isContinue) { + console.debug(/\s/.test(finalMesSend[finalMesSend.length - 1].message.slice(-1))); + finalMesSend[finalMesSend.length - 1].message += + /\s/.test(finalMesSend[finalMesSend.length - 1].message.slice(-1)) + ? zeroDepthAnchor + : `${zeroDepthAnchor}`; + } + + let cfgPrompt = {}; + if (cfgGuidanceScale && cfgGuidanceScale?.value !== 1) { + cfgPrompt = getCfgPrompt(cfgGuidanceScale, isNegative); + } + + if (cfgPrompt && cfgPrompt?.value) { + if (cfgPrompt?.depth === 0) { + finalMesSend[finalMesSend.length - 1].message += + /\s/.test(finalMesSend[finalMesSend.length - 1].message.slice(-1)) + ? cfgPrompt.value + : ` ${cfgPrompt.value}`; + } else { + // TODO: Make all extension prompts use an array/splice method + const lengthDiff = mesSend.length - cfgPrompt.depth; + const cfgDepth = lengthDiff >= 0 ? lengthDiff : 0; + finalMesSend[cfgDepth].extensionPrompts.push(`${cfgPrompt.value}\n`); + } + } + + // Add prompt bias after everything else + // Always run with continue + if (!isInstruct && !isImpersonate) { + if (promptBias.trim().length !== 0) { + finalMesSend[finalMesSend.length - 1].message += + /\s/.test(finalMesSend[finalMesSend.length - 1].message.slice(-1)) + ? promptBias.trimStart() + : ` ${promptBias.trimStart()}`; + } + } + + // Prune from prompt cache if it exists + if (generatedPromptCache.length !== 0) { + generatedPromptCache = cleanupPromptCache(generatedPromptCache); + } + + // Flattens the multiple prompt objects to a string. + const combine = () => { + // Right now, everything is suffixed with a newline + mesSendString = finalMesSend.map((e) => `${e.extensionPrompts.join('')}${e.message}`).join(''); + + // add a custom dingus (if defined) + mesSendString = addChatsSeparator(mesSendString); + + // add chat preamble + mesSendString = addChatsPreamble(mesSendString); + + let combinedPrompt = beforeScenarioAnchor + + storyString + + afterScenarioAnchor + + mesExmString + + mesSendString + + generatedPromptCache; + + combinedPrompt = combinedPrompt.replace(/\r/gm, ''); + + if (power_user.collapse_newlines) { + combinedPrompt = collapseNewlines(combinedPrompt); + } + + return combinedPrompt; + }; + + let data = { + api: main_api, + combinedPrompt: null, + description, + personality, + persona, + scenario, + char: name2, + user: name1, + beforeScenarioAnchor, + afterScenarioAnchor, + mesExmString, + finalMesSend, + generatedPromptCache, + main: system, + jailbreak, + naiPreamble: nai_settings.preamble, + }; + + // Before returning the combined prompt, give available context related information to all subscribers. + eventSource.emitAndWait(event_types.GENERATE_BEFORE_COMBINE_PROMPTS, data); + + // If one or multiple subscribers return a value, forfeit the responsibillity of flattening the context. + return !data.combinedPrompt ? combine() : data.combinedPrompt; + } + + // Get the negative prompt first since it has the unmodified mesSend array + let negativePrompt = main_api == 'textgenerationwebui' ? getCombinedPrompt(true) : undefined; + let finalPrompt = getCombinedPrompt(false); + + // Include the entire guidance scale object + const cfgValues = cfgGuidanceScale && cfgGuidanceScale?.value !== 1 ? ({ guidanceScale: cfgGuidanceScale, negativePrompt: negativePrompt }) : null; + + let maxLength = Number(amount_gen); // how many tokens the AI will be requested to generate + let thisPromptBits = []; + + // TODO: Make this a switch + if (main_api == 'koboldhorde' && horde_settings.auto_adjust_response_length) { + maxLength = Math.min(maxLength, adjustedParams.maxLength); + maxLength = Math.max(maxLength, MIN_LENGTH); // prevent validation errors + } + + let generate_data; + if (main_api == 'koboldhorde' || main_api == 'kobold') { + generate_data = { + prompt: finalPrompt, + gui_settings: true, + max_length: maxLength, + max_context_length: max_context, + }; + + if (preset_settings != 'gui') { + const isHorde = main_api == 'koboldhorde'; + const presetSettings = koboldai_settings[koboldai_setting_names[preset_settings]]; + const maxContext = (adjustedParams && horde_settings.auto_adjust_context_length) ? adjustedParams.maxContextLength : max_context; + generate_data = getKoboldGenerationData(finalPrompt, presetSettings, maxLength, maxContext, isHorde, type); + } + } + else if (main_api == 'textgenerationwebui') { + generate_data = getTextGenGenerationData(finalPrompt, maxLength, isImpersonate, isContinue, cfgValues, type); + } + else if (main_api == 'novel') { + const presetSettings = novelai_settings[novelai_setting_names[nai_settings.preset_settings_novel]]; + generate_data = getNovelGenerationData(finalPrompt, presetSettings, maxLength, isImpersonate, isContinue, cfgValues, type); + } + else if (main_api == 'openai') { + let [prompt, counts] = await prepareOpenAIMessages({ + name2: name2, + charDescription: description, + charPersonality: personality, + Scenario: scenario, + worldInfoBefore: worldInfoBefore, + worldInfoAfter: worldInfoAfter, + extensionPrompts: extension_prompts, + bias: promptBias, + type: type, + quietPrompt: quiet_prompt, + quietImage: quietImage, + cyclePrompt: cyclePrompt, + systemPromptOverride: system, + jailbreakPromptOverride: jailbreak, + personaDescription: persona, + messages: oaiMessages, + messageExamples: oaiMessageExamples, + }, dryRun); + generate_data = { prompt: prompt }; + + // counts will return false if the user has not enabled the token breakdown feature + if (counts) { + parseTokenCounts(counts, thisPromptBits); + } + + if (!dryRun) { + setInContextMessages(openai_messages_count, type); + } + } + + async function finishGenerating() { + if (dryRun) return { error: 'dryRun' }; + + if (power_user.console_log_prompts) { + console.log(generate_data.prompt); + } + + console.debug('rungenerate calling API'); + + showStopButton(); + + //set array object for prompt token itemization of this message + let currentArrayEntry = Number(thisPromptBits.length - 1); + let additionalPromptStuff = { + ...thisPromptBits[currentArrayEntry], + rawPrompt: generate_data.prompt || generate_data.input, + mesId: getNextMessageId(type), + allAnchors: allAnchors, + summarizeString: (extension_prompts['1_memory']?.value || ''), + authorsNoteString: (extension_prompts['2_floating_prompt']?.value || ''), + smartContextString: (extension_prompts['chromadb']?.value || ''), + worldInfoString: worldInfoString, + storyString: storyString, + beforeScenarioAnchor: beforeScenarioAnchor, + afterScenarioAnchor: afterScenarioAnchor, + examplesString: examplesString, + mesSendString: mesSendString, + generatedPromptCache: generatedPromptCache, + promptBias: promptBias, + finalPrompt: finalPrompt, + charDescription: description, + charPersonality: personality, + scenarioText: scenario, + this_max_context: this_max_context, + padding: power_user.token_padding, + main_api: main_api, + instruction: isInstruct ? substituteParams(power_user.prefer_character_prompt && system ? system : power_user.instruct.system_prompt) : '', + userPersona: (power_user.persona_description || ''), + }; + + thisPromptBits = additionalPromptStuff; + + //console.log(thisPromptBits); + const itemizedIndex = itemizedPrompts.findIndex((item) => item.mesId === thisPromptBits['mesId']); + + if (itemizedIndex !== -1) { + itemizedPrompts[itemizedIndex] = thisPromptBits; + } + else { + itemizedPrompts.push(thisPromptBits); + } + + console.debug(`pushed prompt bits to itemizedPrompts array. Length is now: ${itemizedPrompts.length}`); + + if (isStreamingEnabled() && type !== 'quiet') { + streamingProcessor = new StreamingProcessor(type, force_name2, generation_started, message_already_generated); + if (isContinue) { + // Save reply does add cycle text to the prompt, so it's not needed here + streamingProcessor.firstMessageText = ''; + } + + streamingProcessor.generator = await sendStreamingRequest(type, generate_data); + + hideSwipeButtons(); + let getMessage = await streamingProcessor.generate(); + let messageChunk = cleanUpMessage(getMessage, isImpersonate, isContinue, false); + + if (isContinue) { + getMessage = continue_mag + getMessage; + } + + if (streamingProcessor && !streamingProcessor.isStopped && streamingProcessor.isFinished) { + await streamingProcessor.onFinishStreaming(streamingProcessor.messageId, getMessage); + streamingProcessor = null; + triggerAutoContinue(messageChunk, isImpersonate); + } + } else { + return await sendGenerationRequest(type, generate_data); + } + } + + return finishGenerating().then(onSuccess, onError); + + async function onSuccess(data) { + if (!data) return; + let messageChunk = ''; + + if (data.error == 'dryRun') { + generatedPromptCache = ''; + return; + } + + if (!data.error) { + //const getData = await response.json(); + let getMessage = extractMessageFromData(data); + let title = extractTitleFromData(data); + kobold_horde_model = title; + + const swipes = extractMultiSwipes(data, type); + + messageChunk = cleanUpMessage(getMessage, isImpersonate, isContinue, false); + + if (isContinue) { + getMessage = continue_mag + getMessage; + } + + //Formating + const displayIncomplete = type === 'quiet' && !quietToLoud; + getMessage = cleanUpMessage(getMessage, isImpersonate, isContinue, displayIncomplete); + + if (getMessage.length > 0) { + if (isImpersonate) { + $('#send_textarea').val(getMessage).trigger('input'); + generatedPromptCache = ''; + await eventSource.emit(event_types.IMPERSONATE_READY, getMessage); + } + else if (type == 'quiet') { + unblockGeneration(); + return getMessage; + } + else { + // Without streaming we'll be having a full message on continuation. Treat it as a last chunk. + if (originalType !== 'continue') { + ({ type, getMessage } = await saveReply(type, getMessage, false, title, swipes)); + } + else { + ({ type, getMessage } = await saveReply('appendFinal', getMessage, false, title, swipes)); + } + } + + if (type !== 'quiet') { + playMessageSound(); + } + } else { + // If maxLoops is not passed in (e.g. first time generating), set it to MAX_GENERATION_LOOPS + maxLoops ??= MAX_GENERATION_LOOPS; + + if (maxLoops === 0) { + if (type !== 'quiet') { + throwCircuitBreakerError(); + } + throw new Error('Generate circuit breaker interruption'); + } + + // regenerate with character speech reenforced + // to make sure we leave on swipe type while also adding the name2 appendage + await delay(1000); + // The first await is for waiting for the generate to start. The second one is waiting for it to finish + const result = await await Generate(type, { automatic_trigger, force_name2: true, quiet_prompt, skipWIAN, force_chid, maxLoops: maxLoops - 1 }); + return result; + } + + if (power_user.auto_swipe) { + console.debug('checking for autoswipeblacklist on non-streaming message'); + function containsBlacklistedWords(getMessage, blacklist, threshold) { + console.debug('checking blacklisted words'); + const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi'); + const matches = getMessage.match(regex) || []; + return matches.length >= threshold; + } + + const generatedTextFiltered = (getMessage) => { + if (power_user.auto_swipe_blacklist_threshold) { + if (containsBlacklistedWords(getMessage, power_user.auto_swipe_blacklist, power_user.auto_swipe_blacklist_threshold)) { + console.debug('Generated text has blacklisted words'); + return true; + } + } + + return false; + }; + if (generatedTextFiltered(getMessage)) { + console.debug('swiping right automatically'); + is_send_press = false; + swipe_right(); + // TODO: do we want to resolve after an auto-swipe? + return; + } + } + } else { + generatedPromptCache = ''; + + if (data?.response) { + toastr.error(data.response, 'API Error'); + } + throw data?.response; + } + + console.debug('/api/chats/save called by /Generate'); + await saveChatConditional(); + unblockGeneration(); + streamingProcessor = null; + + if (type !== 'quiet') { + triggerAutoContinue(messageChunk, isImpersonate); + } + } + + function onError(exception) { + if (typeof exception?.error?.message === 'string') { + toastr.error(exception.error.message, 'Error', { timeOut: 10000, extendedTimeOut: 20000 }); + } + + unblockGeneration(); + console.log(exception); + streamingProcessor = null; + throw exception; } } From 789954975464290a34a36c43bb19da9e36c227dd Mon Sep 17 00:00:00 2001 From: valadaptive Date: Mon, 25 Dec 2023 03:29:14 -0500 Subject: [PATCH 038/522] Make "send message from chat box" into a function Right now all it does is handle returning if there's already a message being generated, but I'll extend it with more logic that I want to move out of Generate(). --- public/script.js | 15 +++++++++------ public/scripts/RossAscends-mods.js | 6 +++--- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/public/script.js b/public/script.js index 4cdd68979..ebe0654b5 100644 --- a/public/script.js +++ b/public/script.js @@ -1472,6 +1472,14 @@ export async function reloadCurrentChat() { showSwipeButtons(); } +/** + * Send the message currently typed into the chat box. + */ +export function sendTextareaMessage() { + if (is_send_press) return; + Generate(); +} + function messageFormatting(mes, ch_name, isSystem, isUser) { if (!mes) { return ''; @@ -7971,12 +7979,7 @@ jQuery(async function () { }); $('#send_but').on('click', function () { - if (is_send_press == false) { - // This prevents from running /trigger command with a send button - // But send on Enter doesn't set is_send_press (it is done by the Generate itself) - // is_send_press = true; - Generate(); - } + sendTextareaMessage(); }); //menu buttons setup diff --git a/public/scripts/RossAscends-mods.js b/public/scripts/RossAscends-mods.js index 7a0c648f3..46e76e97f 100644 --- a/public/scripts/RossAscends-mods.js +++ b/public/scripts/RossAscends-mods.js @@ -1,5 +1,4 @@ import { - Generate, characters, online_status, main_api, @@ -18,6 +17,7 @@ import { menu_type, substituteParams, callPopup, + sendTextareaMessage, } from '../script.js'; import { @@ -954,9 +954,9 @@ export function initRossMods() { //Enter to send when send_textarea in focus if ($(':focus').attr('id') === 'send_textarea') { const sendOnEnter = shouldSendOnEnter(); - if (!event.shiftKey && !event.ctrlKey && !event.altKey && event.key == 'Enter' && is_send_press == false && sendOnEnter) { + if (!event.shiftKey && !event.ctrlKey && !event.altKey && event.key == 'Enter' && sendOnEnter) { event.preventDefault(); - Generate(); + sendTextareaMessage(); } } if ($(':focus').attr('id') === 'dialogue_popup_input' && !isMobile()) { From 3c0207f6cbcac0812ba137e27fb6e3792c2e63d7 Mon Sep 17 00:00:00 2001 From: valadaptive Date: Mon, 25 Dec 2023 03:32:26 -0500 Subject: [PATCH 039/522] Move "continue on send" logic out of Generate() --- public/script.js | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/public/script.js b/public/script.js index ebe0654b5..58609f800 100644 --- a/public/script.js +++ b/public/script.js @@ -1477,7 +1477,22 @@ export async function reloadCurrentChat() { */ export function sendTextareaMessage() { if (is_send_press) return; - Generate(); + + let generateType; + // "Continue on send" is activated when the user hits "send" (or presses enter) on an empty chat box, and the last + // message was sent from a character (not the user or the system). + const textareaText = String($('#send_textarea').val()); + if (power_user.continue_on_send && + !textareaText && + !selected_group && + chat.length && + !chat[chat.length - 1]['is_user'] && + !chat[chat.length - 1]['is_system'] + ) { + generateType = 'continue'; + } + + Generate(generateType); } function messageFormatting(mes, ch_name, isSystem, isUser) { @@ -3055,10 +3070,6 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu } } - if (!type && !textareaText && power_user.continue_on_send && !selected_group && chat.length && !chat[chat.length - 1]['is_user'] && !chat[chat.length - 1]['is_system']) { - type = 'continue'; - } - const isContinue = type == 'continue'; // Rewrite the generation timer to account for the time passed for all the continuations. From 0f8a16325b66d36505a68ce6922aec6f0bd09cb9 Mon Sep 17 00:00:00 2001 From: valadaptive Date: Mon, 25 Dec 2023 03:45:34 -0500 Subject: [PATCH 040/522] Extract dryRun early return from finishGenerating This means we only have to handle it in one place rather than two. --- public/script.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/public/script.js b/public/script.js index 58609f800..eb6dd72e9 100644 --- a/public/script.js +++ b/public/script.js @@ -3771,9 +3771,12 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu } } - async function finishGenerating() { - if (dryRun) return { error: 'dryRun' }; + if (dryRun) { + generatedPromptCache = ''; + return Promise.resolve(); + } + async function finishGenerating() { if (power_user.console_log_prompts) { console.log(generate_data.prompt); } @@ -3858,11 +3861,6 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu if (!data) return; let messageChunk = ''; - if (data.error == 'dryRun') { - generatedPromptCache = ''; - return; - } - if (!data.error) { //const getData = await response.json(); let getMessage = extractMessageFromData(data); From 29476e7c03e6c83f092bc50dee6088328342911b Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Mon, 25 Dec 2023 13:07:06 +0000 Subject: [PATCH 041/522] add import and export of QR sets --- .../extensions/quick-reply/html/settings.html | 5 +- .../extensions/quick-reply/src/QuickReply.js | 1 + .../quick-reply/src/ui/SettingsUi.js | 84 ++++++++++++++++++- 3 files changed, 87 insertions(+), 3 deletions(-) diff --git a/public/scripts/extensions/quick-reply/html/settings.html b/public/scripts/extensions/quick-reply/html/settings.html index 09cae2872..75a3eb7b1 100644 --- a/public/scripts/extensions/quick-reply/html/settings.html +++ b/public/scripts/extensions/quick-reply/html/settings.html @@ -43,8 +43,11 @@
Edit Quick Replies
- + + + +
diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index 4ea6610a8..27c5e68d2 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -169,6 +169,7 @@ export class QuickReply { del.classList.add('menu_button_icon'); del.classList.add('fa-solid'); del.classList.add('fa-trash-can'); + del.classList.add('redWarningBG'); del.title = 'Remove quick reply'; del.addEventListener('click', ()=>this.delete()); actions.append(del); diff --git a/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js b/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js index d16abf296..a3f926ac5 100644 --- a/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js +++ b/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js @@ -1,6 +1,7 @@ import { callPopup } from '../../../../../script.js'; import { getSortableDelay } from '../../../../utils.js'; -import { warn } from '../../index.js'; +import { log, warn } from '../../index.js'; +import { QuickReply } from '../QuickReply.js'; import { QuickReplySet } from '../QuickReplySet.js'; // eslint-disable-next-line no-unused-vars import { QuickReplySettings } from '../QuickReplySettings.js'; @@ -103,8 +104,16 @@ export class SettingsUi { prepareQrEditor() { // qr editor - this.dom.querySelector('#qr--set-delete').addEventListener('click', async()=>this.deleteQrSet()); this.dom.querySelector('#qr--set-new').addEventListener('click', async()=>this.addQrSet()); + /**@type {HTMLInputElement}*/ + const importFile = this.dom.querySelector('#qr--set-importFile'); + importFile.addEventListener('change', async()=>{ + await this.importQrSet(importFile.files); + importFile.value = null; + }); + this.dom.querySelector('#qr--set-import').addEventListener('click', ()=>importFile.click()); + this.dom.querySelector('#qr--set-export').addEventListener('click', async()=>this.exportQrSet()); + this.dom.querySelector('#qr--set-delete').addEventListener('click', async()=>this.deleteQrSet()); this.dom.querySelector('#qr--set-add').addEventListener('click', async()=>{ this.currentQrSet.addQuickReply(); }); @@ -278,6 +287,77 @@ export class SettingsUi { } } + async importQrSet(/**@type {FileList}*/files) { + for (let i = 0; i < files.length; i++) { + await this.importSingleQrSet(files.item(i)); + } + } + async importSingleQrSet(/**@type {File}*/file) { + log('FILE', file); + try { + const text = await file.text(); + const props = JSON.parse(text); + if (!Number.isInteger(props.version) || typeof props.name != 'string') { + toastr.error(`The file "${file.name}" does not appear to be a valid quick reply set.`); + warn(`The file "${file.name}" does not appear to be a valid quick reply set.`); + } else { + /**@type {QuickReplySet}*/ + const qrs = QuickReplySet.from(JSON.parse(JSON.stringify(props))); + qrs.qrList = props.qrList.map(it=>QuickReply.from(it)); + qrs.init(); + const oldQrs = QuickReplySet.get(props.name); + if (oldQrs) { + const replace = await callPopup(`A Quick Reply Set named "${qrs.name}" already exists.
Do you want to overwrite the existing Quick Reply Set?
The existing set will be deleted. This cannot be undone.`, 'confirm'); + if (replace) { + const idx = QuickReplySet.list.indexOf(oldQrs); + await this.doDeleteQrSet(oldQrs); + QuickReplySet.list.splice(idx, 0, qrs); + await qrs.save(); + this.rerender(); + this.currentSet.value = qrs.name; + this.onQrSetChange(); + this.prepareGlobalSetList(); + this.prepareChatSetList(); + } + } else { + const idx = QuickReplySet.list.findIndex(it=>it.name.localeCompare(qrs.name) == 1); + if (idx > -1) { + QuickReplySet.list.splice(idx, 0, qrs); + } else { + QuickReplySet.list.push(qrs); + } + await qrs.save(); + const opt = document.createElement('option'); { + opt.value = qrs.name; + opt.textContent = qrs.name; + if (idx > -1) { + this.currentSet.children[idx].insertAdjacentElement('beforebegin', opt); + } else { + this.currentSet.append(opt); + } + } + this.currentSet.value = qrs.name; + this.onQrSetChange(); + this.prepareGlobalSetList(); + this.prepareChatSetList(); + } + } + } catch (ex) { + warn(ex); + toastr.error(`Failed to import "${file.name}":\n\n${ex.message}`); + } + } + + exportQrSet() { + const blob = new Blob([JSON.stringify(this.currentQrSet)], { type:'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); { + a.href = url; + a.download = `${this.currentQrSet.name}.json`; + a.click(); + } + } + selectQrSet(qrs) { this.currentSet.value = qrs.name; this.onQrSetChange(); From ef33c6dc61a1b809d6af7da732d3091990dcb05e Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Tue, 26 Dec 2023 11:37:23 +0000 Subject: [PATCH 042/522] don't stop auto-execute on /abort --- .../scripts/extensions/quick-reply/index.js | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/public/scripts/extensions/quick-reply/index.js b/public/scripts/extensions/quick-reply/index.js index e00eaf8ec..719a87332 100644 --- a/public/scripts/extensions/quick-reply/index.js +++ b/public/scripts/extensions/quick-reply/index.js @@ -171,19 +171,23 @@ const init = async () => { } }; + quickReplyApi = new QuickReplyApi(settings, manager); + const slash = new SlashCommandHandler(quickReplyApi); + slash.init(); + if (settings.isEnabled) { const qrList = [ ...settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnStartup)).flat(), ...(settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnStartup))?.flat() ?? []), ]; for (const qr of qrList) { - await qr.onExecute(); + try { + await qr.onExecute(); + } catch (ex) { + warn(ex); + } } } - - quickReplyApi = new QuickReplyApi(settings, manager); - const slash = new SlashCommandHandler(quickReplyApi); - slash.init(); }; eventSource.on(event_types.APP_READY, init); @@ -203,7 +207,11 @@ const onChatChanged = async (chatIdx) => { ...(settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnChatChange))?.flat() ?? []), ]; for (const qr of qrList) { - await qr.onExecute(); + try { + await qr.onExecute(); + } catch (ex) { + warn(ex); + } } } }; @@ -216,7 +224,11 @@ const onUserMessage = async () => { ...(settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnUser))?.flat() ?? []), ]; for (const qr of qrList) { - await qr.onExecute(); + try { + await qr.onExecute(); + } catch (ex) { + warn(ex); + } } } }; @@ -229,7 +241,11 @@ const onAiMessage = async () => { ...(settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnAi))?.flat() ?? []), ]; for (const qr of qrList) { - await qr.onExecute(); + try { + await qr.onExecute(); + } catch (ex) { + warn(ex); + } } } }; From e0a84b0a56169a45d6980993c5ef3ed1aeba9322 Mon Sep 17 00:00:00 2001 From: DonMoralez Date: Tue, 26 Dec 2023 13:45:39 +0200 Subject: [PATCH 043/522] reworked name assignment a bit --- src/endpoints/prompt-converters.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/endpoints/prompt-converters.js b/src/endpoints/prompt-converters.js index 87dbed25a..33ccb9578 100644 --- a/src/endpoints/prompt-converters.js +++ b/src/endpoints/prompt-converters.js @@ -57,15 +57,15 @@ function convertClaudePrompt(messages, addAssistantPostfix, addAssistantPrefill, // Convert messages to the prompt. let requestPrompt = messages.map((v, i) => { - // Set prefix according to the role. - // Claude doesn't support message names, so we'll just add them to the message content. + // Set prefix according to the role. Also, when "Exclude Human/Assistant prefixes" is checked, names are added via the system prefix. let prefix = { - 'assistant': `\n\nAssistant: ${v.name ? `${v.name}: ` : ''}`, - 'user': `\n\nHuman: ${v.name ? `${v.name}: ` : ''}`, + 'assistant': '\n\nAssistant: ', + 'user': '\n\nHuman: ', 'system': i === 0 ? '' : v.name === 'example_assistant' ? '\n\nA: ' : v.name === 'example_user' ? '\n\nH: ' : excludePrefixes && v.name ? `\n\n${v.name}: ` : '\n\n', - 'FixHumMsg': `\n\nFirst message: ${v.name ? `${v.name}: ` : ''}`, + 'FixHumMsg': '\n\nFirst message: ', }[v.role] ?? ''; - return `${prefix}${v.content}`; + // Claude doesn't support message names, so we'll just add them to the message content. + return `${prefix}${v.name && v.role !== 'system' ? `${v.name}: ` : ''}${v.content}`; }).join(''); return requestPrompt; From 89e94edc5709b4e89de48d1b1edf266075f60dc4 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Tue, 26 Dec 2023 12:06:23 +0000 Subject: [PATCH 044/522] add option to prevent recursive auto-execute --- .../extensions/quick-reply/html/qrEditor.html | 4 + .../scripts/extensions/quick-reply/index.js | 60 ++------------- .../quick-reply/src/AutoExecuteHandler.js | 76 +++++++++++++++++++ .../extensions/quick-reply/src/QuickReply.js | 9 +++ 4 files changed, 97 insertions(+), 52 deletions(-) create mode 100644 public/scripts/extensions/quick-reply/src/AutoExecuteHandler.js diff --git a/public/scripts/extensions/quick-reply/html/qrEditor.html b/public/scripts/extensions/quick-reply/html/qrEditor.html index 08b958c0d..d01c8f58b 100644 --- a/public/scripts/extensions/quick-reply/html/qrEditor.html +++ b/public/scripts/extensions/quick-reply/html/qrEditor.html @@ -42,6 +42,10 @@

Auto-Execute

+
-
-
+
+
OpenAI / Claude Reverse Proxy
@@ -735,7 +735,7 @@
-
+
Proxy Password
diff --git a/public/scripts/openai.js b/public/scripts/openai.js index ffb4a9efe..0de1672fd 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -1562,8 +1562,8 @@ async function sendOpenAIRequest(type, messages, signal) { delete generate_data.stop; } - // Proxy is only supported for Claude and OpenAI - if (oai_settings.reverse_proxy && [chat_completion_sources.CLAUDE, chat_completion_sources.OPENAI].includes(oai_settings.chat_completion_source)) { + // Proxy is only supported for Claude, OpenAI and Mistral + if (oai_settings.reverse_proxy && [chat_completion_sources.CLAUDE, chat_completion_sources.OPENAI, chat_completion_sources.MISTRALAI].includes(oai_settings.chat_completion_source)) { validateReverseProxy(); generate_data['reverse_proxy'] = oai_settings.reverse_proxy; generate_data['proxy_password'] = oai_settings.proxy_password; diff --git a/src/endpoints/backends/chat-completions.js b/src/endpoints/backends/chat-completions.js index 712ed4485..d95415a70 100644 --- a/src/endpoints/backends/chat-completions.js +++ b/src/endpoints/backends/chat-completions.js @@ -12,7 +12,7 @@ const { getTokenizerModel, getSentencepiceTokenizer, getTiktokenTokenizer, sente const API_OPENAI = 'https://api.openai.com/v1'; const API_CLAUDE = 'https://api.anthropic.com/v1'; - +const API_MISTRAL = 'https://api.mistral.ai/v1'; /** * Sends a request to Claude API. * @param {express.Request} request Express request @@ -431,7 +431,8 @@ async function sendAI21Request(request, response) { * @param {express.Response} response Express response */ async function sendMistralAIRequest(request, response) { - const apiKey = readSecret(SECRET_KEYS.MISTRALAI); + const apiUrl = new URL(request.body.reverse_proxy || API_MISTRAL).toString(); + const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(SECRET_KEYS.MISTRALAI); if (!apiKey) { console.log('MistralAI API key is missing.'); @@ -495,7 +496,7 @@ async function sendMistralAIRequest(request, response) { console.log('MisralAI request:', requestBody); - const generateResponse = await fetch('https://api.mistral.ai/v1/chat/completions', config); + const generateResponse = await fetch(apiUrl + '/chat/completions', config); if (request.body.stream) { forwardFetchResponse(generateResponse, response); } else { @@ -538,8 +539,8 @@ router.post('/status', jsonParser, async function (request, response_getstatus_o // OpenRouter needs to pass the referer: https://openrouter.ai/docs headers = { 'HTTP-Referer': request.headers.referer }; } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MISTRALAI) { - api_url = 'https://api.mistral.ai/v1'; - api_key_openai = readSecret(SECRET_KEYS.MISTRALAI); + api_url = new URL(request.body.reverse_proxy || API_MISTRAL).toString(); + api_key_openai = request.body.reverse_proxy ? request.body.proxy_password : readSecret(SECRET_KEYS.MISTRALAI); } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.CUSTOM) { api_url = request.body.custom_url; api_key_openai = readSecret(SECRET_KEYS.CUSTOM); From a2e4dc2950c52f8bc45c89cfbcbff016de7483d8 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 31 Dec 2023 04:00:04 +0200 Subject: [PATCH 055/522] Add chunking of vector storage messages --- public/scripts/extensions/vectors/index.js | 76 ++++++++++++++++++- .../scripts/extensions/vectors/manifest.json | 2 +- .../scripts/extensions/vectors/settings.html | 24 ++++-- public/scripts/extensions/vectors/style.css | 4 + 4 files changed, 96 insertions(+), 10 deletions(-) create mode 100644 public/scripts/extensions/vectors/style.css diff --git a/public/scripts/extensions/vectors/index.js b/public/scripts/extensions/vectors/index.js index 9e8777333..c9b28270a 100644 --- a/public/scripts/extensions/vectors/index.js +++ b/public/scripts/extensions/vectors/index.js @@ -1,6 +1,6 @@ import { eventSource, event_types, extension_prompt_types, getCurrentChatId, getRequestHeaders, is_send_press, saveSettingsDebounced, setExtensionPrompt, substituteParams } from '../../../script.js'; import { ModuleWorkerWrapper, extension_settings, getContext, renderExtensionTemplate } from '../../extensions.js'; -import { collapseNewlines, power_user, ui_mode } from '../../power-user.js'; +import { collapseNewlines } from '../../power-user.js'; import { SECRET_KEYS, secret_state } from '../../secrets.js'; import { debounce, getStringHash as calculateHash, waitUntilCondition, onlyUnique, splitRecursive } from '../../utils.js'; @@ -21,6 +21,7 @@ const settings = { protect: 5, insert: 3, query: 2, + message_chunk_size: 400, // For files enabled_files: false, @@ -87,6 +88,29 @@ async function onVectorizeAllClick() { let syncBlocked = false; +/** + * Splits messages into chunks before inserting them into the vector index. + * @param {object[]} items Array of vector items + * @returns {object[]} Array of vector items (possibly chunked) + */ +function splitByChunks(items) { + if (settings.message_chunk_size <= 0) { + return items; + } + + const chunkedItems = []; + + for (const item of items) { + const chunks = splitRecursive(item.text, settings.message_chunk_size); + for (const chunk of chunks) { + const chunkedItem = { ...item, text: chunk }; + chunkedItems.push(chunkedItem); + } + } + + return chunkedItems; +} + async function synchronizeChat(batchSize = 5) { if (!settings.enabled_chats) { return -1; @@ -116,8 +140,9 @@ async function synchronizeChat(batchSize = 5) { const deletedHashes = hashesInCollection.filter(x => !hashedMessages.some(y => y.hash === x)); if (newVectorItems.length > 0) { + const chunkedBatch = splitByChunks(newVectorItems.slice(0, batchSize)); console.log(`Vectors: Found ${newVectorItems.length} new items. Processing ${batchSize}...`); - await insertVectorItems(chatId, newVectorItems.slice(0, batchSize)); + await insertVectorItems(chatId, chunkedBatch); } if (deletedHashes.length > 0) { @@ -492,6 +517,43 @@ function toggleSettings() { $('#vectors_chats_settings').toggle(!!settings.enabled_chats); } +async function onPurgeClick() { + const chatId = getCurrentChatId(); + if (!chatId) { + toastr.info('No chat selected', 'Purge aborted'); + return; + } + await purgeVectorIndex(chatId); + toastr.success('Vector index purged', 'Purge successful'); +} + +async function onViewStatsClick() { + const chatId = getCurrentChatId(); + if (!chatId) { + toastr.info('No chat selected'); + return; + } + + const hashesInCollection = await getSavedHashes(chatId); + const totalHashes = hashesInCollection.length; + const uniqueHashes = hashesInCollection.filter(onlyUnique).length; + + toastr.info(`Total hashes: ${totalHashes}
+ Unique hashes: ${uniqueHashes}

+ I'll mark collected messages with a green circle.`, + `Stats for chat ${chatId}`, + { timeOut: 10000, escapeHtml: false }); + + const chat = getContext().chat; + for (const message of chat) { + if (hashesInCollection.includes(getStringHash(message.mes))) { + const messageElement = $(`.mes[mesid="${chat.indexOf(message)}"]`); + messageElement.addClass('vectorized'); + } + } + +} + jQuery(async () => { if (!extension_settings.vectors) { extension_settings.vectors = settings; @@ -554,9 +616,9 @@ jQuery(async () => { Object.assign(extension_settings.vectors, settings); saveSettingsDebounced(); }); - $('#vectors_advanced_settings').toggleClass('displayNone', power_user.ui_mode === ui_mode.SIMPLE); - $('#vectors_vectorize_all').on('click', onVectorizeAllClick); + $('#vectors_purge').on('click', onPurgeClick); + $('#vectors_view_stats').on('click', onViewStatsClick); $('#vectors_size_threshold').val(settings.size_threshold).on('input', () => { settings.size_threshold = Number($('#vectors_size_threshold').val()); @@ -582,6 +644,12 @@ jQuery(async () => { saveSettingsDebounced(); }); + $('#vectors_message_chunk_size').val(settings.message_chunk_size).on('input', () => { + settings.message_chunk_size = Number($('#vectors_message_chunk_size').val()); + Object.assign(extension_settings.vectors, settings); + saveSettingsDebounced(); + }); + toggleSettings(); eventSource.on(event_types.MESSAGE_DELETED, onChatEvent); eventSource.on(event_types.MESSAGE_EDITED, onChatEvent); diff --git a/public/scripts/extensions/vectors/manifest.json b/public/scripts/extensions/vectors/manifest.json index 7f84c2147..48b40a173 100644 --- a/public/scripts/extensions/vectors/manifest.json +++ b/public/scripts/extensions/vectors/manifest.json @@ -5,7 +5,7 @@ "optional": [], "generate_interceptor": "vectors_rearrangeChat", "js": "index.js", - "css": "", + "css": "style.css", "author": "Cohee#1207", "version": "1.0.0", "homePage": "https://github.com/SillyTavern/SillyTavern" diff --git a/public/scripts/extensions/vectors/settings.html b/public/scripts/extensions/vectors/settings.html index b1d74c83d..626084d58 100644 --- a/public/scripts/extensions/vectors/settings.html +++ b/public/scripts/extensions/vectors/settings.html @@ -75,7 +75,7 @@
-
+
@@ -97,17 +97,23 @@
+
+ + +
- +
- +
@@ -115,8 +121,16 @@ Old messages are vectorized gradually as you chat. To process all previous messages, click the button below. - diff --git a/public/script.js b/public/script.js index fb16babd8..adf3e1eaf 100644 --- a/public/script.js +++ b/public/script.js @@ -6973,7 +6973,7 @@ function openAlternateGreetings() { }); updateAlternateGreetingsHintVisibility(template); - callPopup(template, 'alternate_greeting'); + callPopup(template, 'alternate_greeting', '', { wide: true, large: true }); } function addAlternateGreeting(template, greeting, index, getArray) { From 5594aa456b3d212eb31e11722e42a1df6e71cd65 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sat, 6 Jan 2024 18:21:08 +0000 Subject: [PATCH 123/522] fix jQuery sortable breaking select elements --- .../scripts/extensions/quick-reply/src/QuickReplySetLink.js | 2 ++ public/scripts/extensions/quick-reply/src/ui/SettingsUi.js | 1 + public/scripts/extensions/quick-reply/style.css | 6 ++++++ public/scripts/extensions/quick-reply/style.less | 6 ++++++ 4 files changed, 15 insertions(+) diff --git a/public/scripts/extensions/quick-reply/src/QuickReplySetLink.js b/public/scripts/extensions/quick-reply/src/QuickReplySetLink.js index b95666962..4f7425525 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReplySetLink.js +++ b/public/scripts/extensions/quick-reply/src/QuickReplySetLink.js @@ -39,6 +39,8 @@ export class QuickReplySetLink { } const set = document.createElement('select'); { set.classList.add('qr--set'); + // fix for jQuery sortable breaking childrens' touch events + set.addEventListener('touchstart', (evt)=>evt.stopPropagation()); set.addEventListener('change', ()=>{ this.set = QuickReplySet.get(set.value); this.update(); diff --git a/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js b/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js index 09e842ac9..9bd04b7a0 100644 --- a/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js +++ b/public/scripts/extensions/quick-reply/src/ui/SettingsUi.js @@ -158,6 +158,7 @@ export class SettingsUi { // @ts-ignore $(qrsDom).sortable({ delay: getSortableDelay(), + handle: '.drag-handle', stop: ()=>this.onQrListSort(), }); } diff --git a/public/scripts/extensions/quick-reply/style.css b/public/scripts/extensions/quick-reply/style.css index e39f0fc7d..74bf8134f 100644 --- a/public/scripts/extensions/quick-reply/style.css +++ b/public/scripts/extensions/quick-reply/style.css @@ -156,6 +156,9 @@ align-items: baseline; padding: 0 0.5em; } +#qr--settings .qr--setList > .qr--item > .drag-handle { + padding: 0.75em; +} #qr--settings .qr--setList > .qr--item > .qr--visible { flex: 0 0 auto; display: flex; @@ -189,6 +192,9 @@ #qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(5) { flex: 0 0 auto; } +#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > .drag-handle { + padding: 0.75em; +} #qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemLabel, #qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--action { margin: 0; diff --git a/public/scripts/extensions/quick-reply/style.less b/public/scripts/extensions/quick-reply/style.less index 1f440ea67..e6271a2c0 100644 --- a/public/scripts/extensions/quick-reply/style.less +++ b/public/scripts/extensions/quick-reply/style.less @@ -174,6 +174,9 @@ gap: 0.5em; align-items: baseline; padding: 0 0.5em; + > .drag-handle { + padding: 0.75em; + } > .qr--visible { flex: 0 0 auto; display: flex; @@ -200,6 +203,9 @@ > :nth-child(3) { flex: 0 0 auto; } > :nth-child(4) { flex: 1 1 75%; } > :nth-child(5) { flex: 0 0 auto; } + > .drag-handle { + padding: 0.75em; + } .qr--set-itemLabel, .qr--action { margin: 0; } From 35e8a9835281057c0004aa6d13c29b5fcc8d0275 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 7 Jan 2024 01:22:43 +0200 Subject: [PATCH 124/522] Reorder APIs, add KoboldCpp API hint --- public/index.html | 9 ++++++--- public/scripts/kai-settings.js | 7 +++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/public/index.html b/public/index.html index 6b2bfaa8b..d19e4dfd5 100644 --- a/public/index.html +++ b/public/index.html @@ -1665,11 +1665,11 @@
@@ -1739,6 +1739,9 @@

API url

Example: http://127.0.0.1:5000/api +
+ We have a dedicated KoboldCpp support under Text Completion ⇒ KoboldCpp. +
diff --git a/public/scripts/kai-settings.js b/public/scripts/kai-settings.js index f2f295dc3..b6d6b73b7 100644 --- a/public/scripts/kai-settings.js +++ b/public/scripts/kai-settings.js @@ -308,6 +308,11 @@ const sliders = [ }, ]; +/** + * Sets the supported feature flags for the KoboldAI backend. + * @param {string} koboldUnitedVersion Kobold United version + * @param {string} koboldCppVersion KoboldCPP version + */ export function setKoboldFlags(koboldUnitedVersion, koboldCppVersion) { kai_flags.can_use_stop_sequence = versionCompare(koboldUnitedVersion, MIN_STOP_SEQUENCE_VERSION); kai_flags.can_use_streaming = versionCompare(koboldCppVersion, MIN_STREAMING_KCPPVERSION); @@ -316,6 +321,8 @@ export function setKoboldFlags(koboldUnitedVersion, koboldCppVersion) { kai_flags.can_use_mirostat = versionCompare(koboldCppVersion, MIN_MIROSTAT_KCPPVERSION); kai_flags.can_use_grammar = versionCompare(koboldCppVersion, MIN_GRAMMAR_KCPPVERSION); kai_flags.can_use_min_p = versionCompare(koboldCppVersion, MIN_MIN_P_KCPPVERSION); + const isKoboldCpp = versionCompare(koboldCppVersion, '1.0.0'); + $('#koboldcpp_hint').toggleClass('displayNone', !isKoboldCpp); } /** From fffdd8e5d899c0b3097960f4ad45457ef506e883 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sun, 7 Jan 2024 00:11:30 +0000 Subject: [PATCH 125/522] fix missing substituteParams when executing non-command QRs --- public/scripts/extensions/quick-reply/src/QuickReplySet.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/scripts/extensions/quick-reply/src/QuickReplySet.js b/public/scripts/extensions/quick-reply/src/QuickReplySet.js index 3fcae6c83..e746672d6 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReplySet.js +++ b/public/scripts/extensions/quick-reply/src/QuickReplySet.js @@ -1,4 +1,4 @@ -import { getRequestHeaders } from '../../../../script.js'; +import { getRequestHeaders, substituteParams } from '../../../../script.js'; import { executeSlashCommands } from '../../../slash-commands.js'; import { debounceAsync, warn } from '../index.js'; import { QuickReply } from './QuickReply.js'; @@ -123,7 +123,7 @@ export class QuickReplySet { return typeof result === 'object' ? result?.pipe : ''; } - ta.value = input; + ta.value = substituteParams(input); ta.focus(); if (!this.disableSend) { From 00041ca01ac0854dca8c98f20c0a01e84063c921 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sun, 7 Jan 2024 00:16:44 +0000 Subject: [PATCH 126/522] fix missing return from execute --- public/scripts/extensions/quick-reply/src/QuickReply.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index e01666670..e247aa940 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -464,7 +464,7 @@ export class QuickReply { const message = this.message.replace(/\{\{arg::([^}]+)\}\}/g, (_, key) => { return args[key] ?? ''; }); - this.onExecute(this, message, args.isAutoExecute ?? false); + return await this.onExecute(this, message, args.isAutoExecute ?? false); } } From 9e34804ab0a524b517bb8a3a3533212e61cb0da3 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sun, 7 Jan 2024 10:51:13 +0000 Subject: [PATCH 127/522] add option on WI entries to prevent further recursion --- public/index.html | 8 +++++++- public/scripts/world-info.js | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/public/index.html b/public/index.html index d19e4dfd5..6ed99477f 100644 --- a/public/index.html +++ b/public/index.html @@ -4337,7 +4337,13 @@ +
diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js index 67ed6a9ee..1e21c7d1f 100644 --- a/public/scripts/world-info.js +++ b/public/scripts/world-info.js @@ -757,6 +757,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) { const originalDataKeyMap = { 'displayIndex': 'extensions.display_index', 'excludeRecursion': 'extensions.exclude_recursion', + 'preventRecursion': 'extensions.prevent_recursion', 'selectiveLogic': 'selectiveLogic', 'comment': 'comment', 'constant': 'constant', @@ -1326,6 +1327,18 @@ function getWorldEntry(name, data, entry) { }); excludeRecursionInput.prop('checked', entry.excludeRecursion).trigger('input'); + // prevent recursion + const preventRecursionInput = template.find('input[name="prevent_recursion"]'); + preventRecursionInput.data('uid', entry.uid); + preventRecursionInput.on('input', function () { + const uid = $(this).data('uid'); + const value = $(this).prop('checked'); + data.entries[uid].preventRecursion = value; + setOriginalDataValue(data, uid, 'extensions.prevent_recursion', data.entries[uid].preventRecursion); + saveWorldInfo(name, data); + }); + preventRecursionInput.prop('checked', entry.preventRecursion).trigger('input'); + // delete button const deleteButton = template.find('.delete_entry_button'); deleteButton.data('uid', entry.uid); @@ -1886,6 +1899,7 @@ async function checkWorldInfo(chat, maxContext) { if (needsToScan) { const text = newEntries .filter(x => !failedProbabilityChecks.has(x)) + .filter(x => !x.preventRecursion) .map(x => x.content).join('\n'); const currentlyActivatedText = transformString(text); textToScan = (currentlyActivatedText + '\n' + textToScan); @@ -2158,6 +2172,7 @@ function convertCharacterBook(characterBook) { order: entry.insertion_order, position: entry.extensions?.position ?? (entry.position === 'before_char' ? world_info_position.before : world_info_position.after), excludeRecursion: entry.extensions?.exclude_recursion ?? false, + preventRecursion: entry.extensions?.prevent_recursion ?? false, disable: !entry.enabled, addMemo: entry.comment ? true : false, displayIndex: entry.extensions?.display_index ?? index, From 53c86c66eb44eb61fea5e88d55ca3e04bf6fda85 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sun, 7 Jan 2024 11:13:56 +0000 Subject: [PATCH 128/522] fix API added sets without listeners fixes #1647 --- .../extensions/quick-reply/src/QuickReplyConfig.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/public/scripts/extensions/quick-reply/src/QuickReplyConfig.js b/public/scripts/extensions/quick-reply/src/QuickReplyConfig.js index 872e33e30..ace66ec47 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReplyConfig.js +++ b/public/scripts/extensions/quick-reply/src/QuickReplyConfig.js @@ -38,9 +38,10 @@ export class QuickReplyConfig { const qrl = new QuickReplySetLink(); qrl.set = qrs; qrl.isVisible = isVisible; + this.hookQuickReplyLink(qrl); this.setList.push(qrl); + this.setListDom.append(qrl.renderSettings(this.setList.length - 1)); this.update(); - this.updateSetListDom(); } } removeSet(qrs) { @@ -59,12 +60,7 @@ export class QuickReplyConfig { /**@type {HTMLElement}*/ this.setListDom = root.querySelector('.qr--setList'); root.querySelector('.qr--setListAdd').addEventListener('click', ()=>{ - const qrl = new QuickReplySetLink(); - qrl.set = QuickReplySet.list[0]; - this.hookQuickReplyLink(qrl); - this.setList.push(qrl); - this.setListDom.append(qrl.renderSettings(this.setList.length - 1)); - this.update(); + this.addSet(QuickReplySet.list[0]); }); this.updateSetListDom(); } From 49483e2e21f4189c482301f5ad0453efebcab3bd Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Sun, 7 Jan 2024 11:36:44 +0000 Subject: [PATCH 129/522] add optional arguments to /world command - deactivate a single world - toggle a world - suppress toast messages --- public/scripts/world-info.js | 44 +++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js index 67ed6a9ee..d0fc83f05 100644 --- a/public/scripts/world-info.js +++ b/public/scripts/world-info.js @@ -2265,25 +2265,53 @@ export async function importEmbeddedWorldInfo(skipPopup = false) { setWorldInfoButtonClass(chid, true); } -function onWorldInfoChange(_, text) { - if (_ !== '__notSlashCommand__') { // if it's a slash command +function onWorldInfoChange(args, text) { + if (args !== '__notSlashCommand__') { // if it's a slash command + const silent = args.silent == 'true'; if (text.trim() !== '') { // and args are provided const slashInputSplitText = text.trim().toLowerCase().split(','); slashInputSplitText.forEach((worldName) => { const wiElement = getWIElement(worldName); if (wiElement.length > 0) { - selected_world_info.push(wiElement.text()); - wiElement.prop('selected', true); - toastr.success(`Activated world: ${wiElement.text()}`); + const name = wiElement.text(); + switch (args.state) { + case 'off': { + if (selected_world_info.includes(name)) { + selected_world_info.splice(selected_world_info.indexOf(name), 1); + wiElement.prop('selected', false); + if (!silent) toastr.success(`Deactivated world: ${name}`); + } else { + if (!silent) toastr.error(`World was not active: ${name}`); + } + break; + } + case 'toggle': { + if (selected_world_info.includes(name)) { + selected_world_info.splice(selected_world_info.indexOf(name), 1); + wiElement.prop('selected', false); + if (!silent) toastr.success(`Activated world: ${name}`); + } else { + selected_world_info.push(name); + wiElement.prop('selected', true); + if (!silent) toastr.success(`Deactivated world: ${name}`); + } + break; + } + default: { + selected_world_info.push(name); + wiElement.prop('selected', true); + if (!silent) toastr.success(`Activated world: ${name}`); + } + } } else { - toastr.error(`No world found named: ${worldName}`); + if (!silent) toastr.error(`No world found named: ${worldName}`); } }); $('#world_info').trigger('change'); } else { // if no args, unset all worlds toastr.success('Deactivated all worlds'); - selected_world_info = []; + if (!silent) selected_world_info = []; $('#world_info').val(null).trigger('change'); } } else { //if it's a pointer selection @@ -2414,7 +2442,7 @@ function assignLorebookToChat() { jQuery(() => { $(document).ready(function () { - registerSlashCommand('world', onWorldInfoChange, [], '(optional name) – sets active World, or unsets if no args provided', true, true); + registerSlashCommand('world', onWorldInfoChange, [], '[optional state=off|toggle] [optional silent=true] (optional name) – sets active World, or unsets if no args provided, use state=off and state=toggle to deactivate or toggle a World, use silent=true to suppress toast messages', true, true); }); From 247048ebfab5e27081c9cd800098aaf3cc7a66ca Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 7 Jan 2024 18:58:30 +0200 Subject: [PATCH 130/522] Use boolean selector --- public/scripts/world-info.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js index d0fc83f05..abb552391 100644 --- a/public/scripts/world-info.js +++ b/public/scripts/world-info.js @@ -2267,7 +2267,7 @@ export async function importEmbeddedWorldInfo(skipPopup = false) { function onWorldInfoChange(args, text) { if (args !== '__notSlashCommand__') { // if it's a slash command - const silent = args.silent == 'true'; + const silent = isTrueBoolean(args.silent); if (text.trim() !== '') { // and args are provided const slashInputSplitText = text.trim().toLowerCase().split(','); From c54746b21c8c84530ce43851f63bfb82cd7d4cf3 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 7 Jan 2024 19:00:16 +0200 Subject: [PATCH 131/522] Fix world unset --- public/scripts/world-info.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js index abb552391..79aabc100 100644 --- a/public/scripts/world-info.js +++ b/public/scripts/world-info.js @@ -2310,8 +2310,8 @@ function onWorldInfoChange(args, text) { }); $('#world_info').trigger('change'); } else { // if no args, unset all worlds - toastr.success('Deactivated all worlds'); - if (!silent) selected_world_info = []; + if (!silent) toastr.success('Deactivated all worlds'); + selected_world_info = []; $('#world_info').val(null).trigger('change'); } } else { //if it's a pointer selection From c92b91604be550aa9e3c03a8acb2792025c12ea2 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 7 Jan 2024 19:05:35 +0200 Subject: [PATCH 132/522] Save flag to character WI --- src/endpoints/characters.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/endpoints/characters.js b/src/endpoints/characters.js index ac8d8187d..1b5fd8528 100644 --- a/src/endpoints/characters.js +++ b/src/endpoints/characters.js @@ -387,6 +387,7 @@ function convertWorldInfoToCharacterBook(name, entries) { depth: entry.depth ?? 4, selectiveLogic: entry.selectiveLogic ?? 0, group: entry.group ?? '', + prevent_recursion: entry.preventRecursion ?? false, }, }; From 46cd6143ac4877c4affcd3fa41ddb4a131ec24b4 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 7 Jan 2024 19:11:58 +0200 Subject: [PATCH 133/522] Fix checkbox alignment --- public/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/index.html b/public/index.html index 6ed99477f..c7f6a041d 100644 --- a/public/index.html +++ b/public/index.html @@ -4367,7 +4367,7 @@ -

DynaTemp -
+

+
+ +
- DynaTemp Range - - + Min Temp + + +
+
+ Max Temp + +
diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index 035ed9d83..1f40c72b2 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -608,7 +608,8 @@ async function CreateZenSliders(elmnt) { sliderID == 'rep_pen_range') { decimals = 0; } - if (sliderID == 'dynatemp_range_textgenerationwebui') { + if (sliderID == 'min_temp_textgenerationwebui' || + sliderID == 'max_temp_textgenerationwebui') { decimals = 2; } if (sliderID == 'eta_cutoff_textgenerationwebui' || @@ -635,13 +636,14 @@ async function CreateZenSliders(elmnt) { sliderID == 'tfs_textgenerationwebui' || sliderID == 'min_p_textgenerationwebui' || sliderID == 'temp_textgenerationwebui' || - sliderID == 'temp' || - sliderID == 'dynatemp_range_textgenerationwebui') { + sliderID == 'temp') { numSteps = 20; } if (sliderID == 'mirostat_eta_textgenerationwebui' || sliderID == 'penalty_alpha_textgenerationwebui' || - sliderID == 'length_penalty_textgenerationwebui') { + sliderID == 'length_penalty_textgenerationwebui' || + sliderID == 'min_temp_textgenerationwebui' || + sliderID == 'max_temp_textgenerationwebui') { numSteps = 50; } //customize off values diff --git a/public/scripts/textgen-settings.js b/public/scripts/textgen-settings.js index ed834c38a..61cafad5e 100644 --- a/public/scripts/textgen-settings.js +++ b/public/scripts/textgen-settings.js @@ -79,7 +79,9 @@ const settings = { presence_pen: 0, do_sample: true, early_stopping: false, - dynatemp_range: 0, + dynatemp: false, + min_temp: 0, + max_temp: 2.0, seed: -1, preset: 'Default', add_bos_token: true, @@ -138,7 +140,9 @@ const setting_names = [ 'num_beams', 'length_penalty', 'min_length', - 'dynatemp_range', + 'dynatemp', + 'min_temp', + 'max_temp', 'encoder_rep_pen', 'freq_pen', 'presence_pen', @@ -720,7 +724,10 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, 'length_penalty': settings.length_penalty, 'early_stopping': settings.early_stopping, 'add_bos_token': settings.add_bos_token, - 'dynatemp_range': settings.dynatemp_range, + 'dynatemp': settings.dynatemp, + 'dynatemp_low': settings.min_temp, + 'dynatemp_high': settings.max_temp, + 'dynatemp_range': (settings.min_temp + settings.max_temp) / 2, 'stopping_strings': getStoppingStrings(isImpersonate, isContinue), 'stop': getStoppingStrings(isImpersonate, isContinue), 'truncation_length': max_context, From 04a5d8390d4d6d74b99323133d40caac28786940 Mon Sep 17 00:00:00 2001 From: Alexander Abushady <44341163+AAbushady@users.noreply.github.com> Date: Mon, 8 Jan 2024 23:58:06 -0500 Subject: [PATCH 149/522] Dynatemp UI v3.1 fixes for html positioning as well as api settings. --- public/index.html | 47 +++++++++++++++--------------- public/scripts/textgen-settings.js | 4 +-- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/public/index.html b/public/index.html index 48ccb4037..f88ad0d04 100644 --- a/public/index.html +++ b/public/index.html @@ -1270,6 +1270,30 @@
--> +
+

+
+
+ + +
+ Dynamic Temperature +
+
+

+
+
+ Minimum Temp + + +
+
+ Maximum Temp + + +
+
+

Mirostat
@@ -1315,29 +1339,6 @@

-
-

DynaTemp -
-

-
-
- -
-
- Min Temp - - -
-
- Max Temp - - -
-
-

Contrast Search
diff --git a/public/scripts/textgen-settings.js b/public/scripts/textgen-settings.js index 61cafad5e..42385ec0a 100644 --- a/public/scripts/textgen-settings.js +++ b/public/scripts/textgen-settings.js @@ -710,7 +710,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, 'model': getModel(), 'max_new_tokens': maxTokens, 'max_tokens': maxTokens, - 'temperature': settings.temp, + 'temperature': settings.dynatemp ? (settings.min_temp + settings.max_temp) / 2 : settings.temp, 'top_p': settings.top_p, 'typical_p': settings.typical_p, 'min_p': settings.min_p, @@ -727,7 +727,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, 'dynatemp': settings.dynatemp, 'dynatemp_low': settings.min_temp, 'dynatemp_high': settings.max_temp, - 'dynatemp_range': (settings.min_temp + settings.max_temp) / 2, + 'dynatemp_range': (settings.max_temp - settings.min_temp) / 2, 'stopping_strings': getStoppingStrings(isImpersonate, isContinue), 'stop': getStoppingStrings(isImpersonate, isContinue), 'truncation_length': max_context, From 5ad980cf999d4f4e32ce4a2221dd2dd45a407eeb Mon Sep 17 00:00:00 2001 From: Alexander Abushady <44341163+AAbushady@users.noreply.github.com> Date: Tue, 9 Jan 2024 00:02:53 -0500 Subject: [PATCH 150/522] Fix for realzies --- public/scripts/textgen-settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/textgen-settings.js b/public/scripts/textgen-settings.js index 42385ec0a..cf1afccb1 100644 --- a/public/scripts/textgen-settings.js +++ b/public/scripts/textgen-settings.js @@ -724,7 +724,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, 'length_penalty': settings.length_penalty, 'early_stopping': settings.early_stopping, 'add_bos_token': settings.add_bos_token, - 'dynatemp': settings.dynatemp, + 'dynamic_temperature': settings.dynatemp, 'dynatemp_low': settings.min_temp, 'dynatemp_high': settings.max_temp, 'dynatemp_range': (settings.max_temp - settings.min_temp) / 2, From ec63cd8b6de6c0b9b5e4cb8cac2efbb6009d6c47 Mon Sep 17 00:00:00 2001 From: Alexander Abushady <44341163+AAbushady@users.noreply.github.com> Date: Tue, 9 Jan 2024 00:54:20 -0500 Subject: [PATCH 151/522] Dynatemp Range Kobold Dynatemp range set when deactivated, now will work properly --- public/scripts/textgen-settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/textgen-settings.js b/public/scripts/textgen-settings.js index cf1afccb1..037bb1b5d 100644 --- a/public/scripts/textgen-settings.js +++ b/public/scripts/textgen-settings.js @@ -727,7 +727,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, 'dynamic_temperature': settings.dynatemp, 'dynatemp_low': settings.min_temp, 'dynatemp_high': settings.max_temp, - 'dynatemp_range': (settings.max_temp - settings.min_temp) / 2, + 'dynatemp_range': settings.dynatemp ? (settings.max_temp - settings.min_temp) / 2 : 0, 'stopping_strings': getStoppingStrings(isImpersonate, isContinue), 'stop': getStoppingStrings(isImpersonate, isContinue), 'truncation_length': max_context, From 1c830865159d42b2795bda5696220506320cefe8 Mon Sep 17 00:00:00 2001 From: Alexander Abushady <44341163+AAbushady@users.noreply.github.com> Date: Tue, 9 Jan 2024 01:12:27 -0500 Subject: [PATCH 152/522] Update temperature max value to 5 For parity's sake --- public/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/index.html b/public/index.html index f88ad0d04..41e71abf0 100644 --- a/public/index.html +++ b/public/index.html @@ -1161,8 +1161,8 @@ Temperature
- - + +

From aa796e5aae9b05e9a844de1be87ef82886ea36e5 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 9 Jan 2024 11:48:51 +0200 Subject: [PATCH 153/522] #1649 Fix deactivation of singular group entry per recursion step --- public/scripts/world-info.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js index 42ca983fb..4a7f8ac6a 100644 --- a/public/scripts/world-info.js +++ b/public/scripts/world-info.js @@ -2004,11 +2004,6 @@ function filterByInclusionGroups(newEntries, allActivatedEntries) { for (const [key, group] of Object.entries(grouped)) { console.debug(`Checking inclusion group '${key}' with ${group.length} entries`, group); - if (!Array.isArray(group) || group.length <= 1) { - console.debug('Skipping inclusion group check, only one entry'); - continue; - } - if (Array.from(allActivatedEntries).some(x => x.group === key)) { console.debug(`Skipping inclusion group check, group already activated '${key}'`); // We need to forcefully deactivate all other entries in the group @@ -2018,6 +2013,11 @@ function filterByInclusionGroups(newEntries, allActivatedEntries) { continue; } + if (!Array.isArray(group) || group.length <= 1) { + console.debug('Skipping inclusion group check, only one entry'); + continue; + } + // Do weighted random using probability of entry as weight const totalWeight = group.reduce((acc, item) => acc + item.probability, 0); const rollValue = Math.random() * totalWeight; From 1bf1f56b38dd944b60acea599db902952f1aa49d Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Tue, 9 Jan 2024 14:24:26 +0000 Subject: [PATCH 154/522] add duplicate world info button --- public/index.html | 1 + public/scripts/world-info.js | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/public/index.html b/public/index.html index c7f6a041d..cb3c04def 100644 --- a/public/index.html +++ b/public/index.html @@ -2915,6 +2915,7 @@ +
@@ -115,9 +115,9 @@ Run On Edit -
From 8a7519c6e75bc3d08dde528506363493225b9af6 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 11 Jan 2024 02:41:00 +0200 Subject: [PATCH 160/522] Replace match with $0 --- public/scripts/extensions/regex/engine.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/public/scripts/extensions/regex/engine.js b/public/scripts/extensions/regex/engine.js index 57d0e2517..358fc15ae 100644 --- a/public/scripts/extensions/regex/engine.js +++ b/public/scripts/extensions/regex/engine.js @@ -112,10 +112,17 @@ function runRegexScript(regexScript, rawString, { characterOverride } = {}) { // Run replacement. Currently does not support the Overlay strategy newString = rawString.replace(findRegex, function(match) { const args = [...arguments]; - const replaceString = regexScript.replaceString.replace('{{match}}', '$1'); + const replaceString = regexScript.replaceString.replace(/{{match}}/gi, '$0'); const replaceWithGroups = replaceString.replaceAll(/\$(\d)+/g, (_, num) => { - // match is found here + // Get a full match or a capture group const match = args[Number(num)]; + + // No match found - return the empty string + if (!match) { + return ''; + } + + // Remove trim strings from the match const filteredMatch = filterString(match, regexScript.trimStrings, { characterOverride }); // TODO: Handle overlay here From a126bd3422c3d5881f60727db6a094b9a447d40f Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 11 Jan 2024 02:42:08 +0200 Subject: [PATCH 161/522] Specify that overlay doesn't work --- public/scripts/extensions/regex/editor.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/extensions/regex/editor.html b/public/scripts/extensions/regex/editor.html index bdcbd66c2..3c3f65aae 100644 --- a/public/scripts/extensions/regex/editor.html +++ b/public/scripts/extensions/regex/editor.html @@ -124,7 +124,7 @@ Replacement Strategy
From 89a999cfd4a274c4aaace5cc87ff5ef50b64304e Mon Sep 17 00:00:00 2001 From: valadaptive Date: Tue, 9 Jan 2024 13:23:51 -0500 Subject: [PATCH 162/522] Move macro substitution to new module substituteParams has become a thin wrapper around the new evaluateMacros function, and will become more of a compatibility shim as refactorings and rewrites are done. --- public/script.js | 271 +------------------------------------- public/scripts/macros.js | 275 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 279 insertions(+), 267 deletions(-) create mode 100644 public/scripts/macros.js diff --git a/public/script.js b/public/script.js index e5624965a..85d33926b 100644 --- a/public/script.js +++ b/public/script.js @@ -16,7 +16,6 @@ import { generateTextGenWithStreaming, getTextGenGenerationData, textgen_types, - textgenerationwebui_banned_in_macros, getTextGenServer, validateTextGenUrl, } from './scripts/textgen-settings.js'; @@ -134,7 +133,6 @@ import { download, isDataURL, getCharaFilename, - isDigitsOnly, PAGINATION_TEMPLATE, waitUntilCondition, escapeRegex, @@ -180,7 +178,6 @@ import { getInstructStoppingSequences, autoSelectInstructPreset, formatInstructModeSystemPrompt, - replaceInstructMacros, } from './scripts/instruct-mode.js'; import { applyLocale, initLocales } from './scripts/i18n.js'; import { getFriendlyTokenizerName, getTokenCount, getTokenizerModel, initTokenizers, saveTokenCache } from './scripts/tokenizers.js'; @@ -190,8 +187,8 @@ import { hideLoader, showLoader } from './scripts/loader.js'; import { BulkEditOverlay, CharacterContextMenu } from './scripts/BulkEditOverlay.js'; import { loadMancerModels, loadOllamaModels, loadTogetherAIModels } from './scripts/textgen-models.js'; import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags } from './scripts/chats.js'; -import { replaceVariableMacros } from './scripts/variables.js'; import { initPresetManager } from './scripts/preset-manager.js'; +import { evaluateMacros } from './scripts/macros.js'; //exporting functions and vars for mods export { @@ -2054,88 +2051,6 @@ function scrollChatToBottom() { } } -/** - * Returns the ID of the last message in the chat. - * @returns {string} The ID of the last message in the chat. - */ -function getLastMessageId() { - const index = chat?.length - 1; - - if (!isNaN(index) && index >= 0) { - return String(index); - } - - return ''; -} - -/** - * Returns the ID of the first message included in the context. - * @returns {string} The ID of the first message in the context. - */ -function getFirstIncludedMessageId() { - const index = document.querySelector('.lastInContext')?.getAttribute('mesid'); - - if (!isNaN(index) && index >= 0) { - return String(index); - } - - return ''; -} - -/** - * Returns the last message in the chat. - * @returns {string} The last message in the chat. - */ -function getLastMessage() { - const index = chat?.length - 1; - - if (!isNaN(index) && index >= 0) { - return chat[index].mes; - } - - return ''; -} - -/** - * Returns the ID of the last swipe. - * @returns {string} The 1-based ID of the last swipe - */ -function getLastSwipeId() { - const index = chat?.length - 1; - - if (!isNaN(index) && index >= 0) { - const swipes = chat[index].swipes; - - if (!Array.isArray(swipes) || swipes.length === 0) { - return ''; - } - - return String(swipes.length); - } - - return ''; -} - -/** - * Returns the ID of the current swipe. - * @returns {string} The 1-based ID of the current swipe. - */ -function getCurrentSwipeId() { - const index = chat?.length - 1; - - if (!isNaN(index) && index >= 0) { - const swipeId = chat[index].swipe_id; - - if (swipeId === undefined || isNaN(swipeId)) { - return ''; - } - - return String(swipeId + 1); - } - - return ''; -} - /** * Substitutes {{macro}} parameters in a string. * @param {string} content - The string to substitute parameters in. @@ -2146,187 +2061,9 @@ function getCurrentSwipeId() { * @returns {string} The string with substituted parameters. */ function substituteParams(content, _name1, _name2, _original, _group, _replaceCharacterCard = true) { - _name1 = _name1 ?? name1; - _name2 = _name2 ?? name2; - _group = _group ?? name2; - - if (!content) { - return ''; - } - - // Replace {{original}} with the original message - // Note: only replace the first instance of {{original}} - // This will hopefully prevent the abuse - if (typeof _original === 'string') { - content = content.replace(/{{original}}/i, _original); - } - content = diceRollReplace(content); - content = replaceInstructMacros(content); - content = replaceVariableMacros(content); - content = content.replace(/{{newline}}/gi, '\n'); - content = content.replace(/{{input}}/gi, String($('#send_textarea').val())); - - if (_replaceCharacterCard) { - const fields = getCharacterCardFields(); - content = content.replace(/{{charPrompt}}/gi, fields.system || ''); - content = content.replace(/{{charJailbreak}}/gi, fields.jailbreak || ''); - content = content.replace(/{{description}}/gi, fields.description || ''); - content = content.replace(/{{personality}}/gi, fields.personality || ''); - content = content.replace(/{{scenario}}/gi, fields.scenario || ''); - content = content.replace(/{{persona}}/gi, fields.persona || ''); - content = content.replace(/{{mesExamples}}/gi, fields.mesExamples || ''); - } - - content = content.replace(/{{maxPrompt}}/gi, () => String(getMaxContextSize())); - content = content.replace(/{{user}}/gi, _name1); - content = content.replace(/{{char}}/gi, _name2); - content = content.replace(/{{charIfNotGroup}}/gi, _group); - content = content.replace(/{{group}}/gi, _group); - content = content.replace(/{{lastMessage}}/gi, getLastMessage()); - content = content.replace(/{{lastMessageId}}/gi, getLastMessageId()); - content = content.replace(/{{firstIncludedMessageId}}/gi, getFirstIncludedMessageId()); - content = content.replace(/{{lastSwipeId}}/gi, getLastSwipeId()); - content = content.replace(/{{currentSwipeId}}/gi, getCurrentSwipeId()); - - content = content.replace(//gi, _name1); - content = content.replace(//gi, _name2); - content = content.replace(//gi, _group); - content = content.replace(//gi, _group); - - content = content.replace(/\{\{\/\/([\s\S]*?)\}\}/gm, ''); - - content = content.replace(/{{time}}/gi, moment().format('LT')); - content = content.replace(/{{date}}/gi, moment().format('LL')); - content = content.replace(/{{weekday}}/gi, moment().format('dddd')); - content = content.replace(/{{isotime}}/gi, moment().format('HH:mm')); - content = content.replace(/{{isodate}}/gi, moment().format('YYYY-MM-DD')); - - content = content.replace(/{{datetimeformat +([^}]*)}}/gi, (_, format) => { - const formattedTime = moment().format(format); - return formattedTime; - }); - content = content.replace(/{{idle_duration}}/gi, () => getTimeSinceLastMessage()); - content = content.replace(/{{time_UTC([-+]\d+)}}/gi, (_, offset) => { - const utcOffset = parseInt(offset, 10); - const utcTime = moment().utc().utcOffset(utcOffset).format('LT'); - return utcTime; - }); - content = bannedWordsReplace(content); - content = randomReplace(content); - return content; + return evaluateMacros(content, _name1 ?? name1, _name2 ?? name2, _original, _group ?? name2, _replaceCharacterCard); } -/** - * Replaces banned words in macros with an empty string. - * Adds them to textgenerationwebui ban list. - * @param {string} inText Text to replace banned words in - * @returns {string} Text without the "banned" macro - */ -function bannedWordsReplace(inText) { - if (!inText) { - return ''; - } - - const banPattern = /{{banned "(.*)"}}/gi; - - if (main_api == 'textgenerationwebui') { - const bans = inText.matchAll(banPattern); - if (bans) { - for (const banCase of bans) { - console.log('Found banned words in macros: ' + banCase[1]); - textgenerationwebui_banned_in_macros.push(banCase[1]); - } - } - } - - inText = inText.replaceAll(banPattern, ''); - return inText; -} - -function getTimeSinceLastMessage() { - const now = moment(); - - if (Array.isArray(chat) && chat.length > 0) { - let lastMessage; - let takeNext = false; - - for (let i = chat.length - 1; i >= 0; i--) { - const message = chat[i]; - - if (message.is_system) { - continue; - } - - if (message.is_user && takeNext) { - lastMessage = message; - break; - } - - takeNext = true; - } - - if (lastMessage?.send_date) { - const lastMessageDate = timestampToMoment(lastMessage.send_date); - const duration = moment.duration(now.diff(lastMessageDate)); - return duration.humanize(); - } - } - - return 'just now'; -} - -function randomReplace(input, emptyListPlaceholder = '') { - const randomPatternNew = /{{random\s?::\s?([^}]+)}}/gi; - const randomPatternOld = /{{random\s?:\s?([^}]+)}}/gi; - - if (randomPatternNew.test(input)) { - return input.replace(randomPatternNew, (match, listString) => { - //split on double colons instead of commas to allow for commas inside random items - const list = listString.split('::').filter(item => item.length > 0); - if (list.length === 0) { - return emptyListPlaceholder; - } - var rng = new Math.seedrandom('added entropy.', { entropy: true }); - const randomIndex = Math.floor(rng() * list.length); - //trim() at the end to allow for empty random values - return list[randomIndex].trim(); - }); - } else if (randomPatternOld.test(input)) { - return input.replace(randomPatternOld, (match, listString) => { - const list = listString.split(',').map(item => item.trim()).filter(item => item.length > 0); - if (list.length === 0) { - return emptyListPlaceholder; - } - var rng = new Math.seedrandom('added entropy.', { entropy: true }); - const randomIndex = Math.floor(rng() * list.length); - return list[randomIndex]; - }); - } else { - return input; - } -} - -function diceRollReplace(input, invalidRollPlaceholder = '') { - const rollPattern = /{{roll[ : ]([^}]+)}}/gi; - - return input.replace(rollPattern, (match, matchValue) => { - let formula = matchValue.trim(); - - if (isDigitsOnly(formula)) { - formula = `1d${formula}`; - } - - const isValid = droll.validate(formula); - - if (!isValid) { - console.debug(`Invalid roll formula: ${formula}`); - return invalidRollPlaceholder; - } - - const result = droll.roll(formula); - return new String(result.total); - }); -} /** * Gets stopping sequences for the prompt. @@ -2569,7 +2306,7 @@ export function baseChatReplace(value, name1, name2) { * Returns the character card fields for the current character. * @returns {{system: string, mesExamples: string, description: string, personality: string, persona: string, scenario: string, jailbreak: string}} */ -function getCharacterCardFields() { +export function getCharacterCardFields() { const result = { system: '', mesExamples: '', description: '', personality: '', persona: '', scenario: '', jailbreak: '' }; const character = characters[this_chid]; @@ -4178,7 +3915,7 @@ export async function sendMessageAsUser(messageText, messageBias, insertAt = nul } } -function getMaxContextSize() { +export function getMaxContextSize() { let this_max_context = 1487; if (main_api == 'kobold' || main_api == 'koboldhorde' || main_api == 'textgenerationwebui') { this_max_context = (max_context - amount_gen); diff --git a/public/scripts/macros.js b/public/scripts/macros.js new file mode 100644 index 000000000..1f6ba20b9 --- /dev/null +++ b/public/scripts/macros.js @@ -0,0 +1,275 @@ +import { chat, main_api, getMaxContextSize, getCharacterCardFields } from '../script'; +import { timestampToMoment, isDigitsOnly } from './utils'; +import { textgenerationwebui_banned_in_macros } from './textgen-settings'; +import { replaceInstructMacros } from './instruct-mode'; +import { replaceVariableMacros } from './variables'; + +/** + * Returns the ID of the last message in the chat. + * @returns {string} The ID of the last message in the chat. + */ +function getLastMessageId() { + const index = chat?.length - 1; + + if (!isNaN(index) && index >= 0) { + return String(index); + } + + return ''; +} + +/** + * Returns the ID of the first message included in the context. + * @returns {string} The ID of the first message in the context. + */ +function getFirstIncludedMessageId() { + const index = document.querySelector('.lastInContext')?.getAttribute('mesid'); + + if (!isNaN(index) && index >= 0) { + return String(index); + } + + return ''; +} + +/** + * Returns the last message in the chat. + * @returns {string} The last message in the chat. + */ +function getLastMessage() { + const index = chat?.length - 1; + + if (!isNaN(index) && index >= 0) { + return chat[index].mes; + } + + return ''; +} + +/** + * Returns the ID of the last swipe. + * @returns {string} The 1-based ID of the last swipe + */ +function getLastSwipeId() { + const index = chat?.length - 1; + + if (!isNaN(index) && index >= 0) { + const swipes = chat[index].swipes; + + if (!Array.isArray(swipes) || swipes.length === 0) { + return ''; + } + + return String(swipes.length); + } + + return ''; +} + +/** + * Returns the ID of the current swipe. + * @returns {string} The 1-based ID of the current swipe. + */ +function getCurrentSwipeId() { + const index = chat?.length - 1; + + if (!isNaN(index) && index >= 0) { + const swipeId = chat[index].swipe_id; + + if (swipeId === undefined || isNaN(swipeId)) { + return ''; + } + + return String(swipeId + 1); + } + + return ''; +} + +/** + * Replaces banned words in macros with an empty string. + * Adds them to textgenerationwebui ban list. + * @param {string} inText Text to replace banned words in + * @returns {string} Text without the "banned" macro + */ +function bannedWordsReplace(inText) { + if (!inText) { + return ''; + } + + const banPattern = /{{banned "(.*)"}}/gi; + + if (main_api == 'textgenerationwebui') { + const bans = inText.matchAll(banPattern); + if (bans) { + for (const banCase of bans) { + console.log('Found banned words in macros: ' + banCase[1]); + textgenerationwebui_banned_in_macros.push(banCase[1]); + } + } + } + + inText = inText.replaceAll(banPattern, ''); + return inText; +} + +function getTimeSinceLastMessage() { + const now = moment(); + + if (Array.isArray(chat) && chat.length > 0) { + let lastMessage; + let takeNext = false; + + for (let i = chat.length - 1; i >= 0; i--) { + const message = chat[i]; + + if (message.is_system) { + continue; + } + + if (message.is_user && takeNext) { + lastMessage = message; + break; + } + + takeNext = true; + } + + if (lastMessage?.send_date) { + const lastMessageDate = timestampToMoment(lastMessage.send_date); + const duration = moment.duration(now.diff(lastMessageDate)); + return duration.humanize(); + } + } + + return 'just now'; +} + +function randomReplace(input, emptyListPlaceholder = '') { + const randomPatternNew = /{{random\s?::\s?([^}]+)}}/gi; + const randomPatternOld = /{{random\s?:\s?([^}]+)}}/gi; + + if (randomPatternNew.test(input)) { + return input.replace(randomPatternNew, (match, listString) => { + //split on double colons instead of commas to allow for commas inside random items + const list = listString.split('::').filter(item => item.length > 0); + if (list.length === 0) { + return emptyListPlaceholder; + } + var rng = new Math.seedrandom('added entropy.', { entropy: true }); + const randomIndex = Math.floor(rng() * list.length); + //trim() at the end to allow for empty random values + return list[randomIndex].trim(); + }); + } else if (randomPatternOld.test(input)) { + return input.replace(randomPatternOld, (match, listString) => { + const list = listString.split(',').map(item => item.trim()).filter(item => item.length > 0); + if (list.length === 0) { + return emptyListPlaceholder; + } + var rng = new Math.seedrandom('added entropy.', { entropy: true }); + const randomIndex = Math.floor(rng() * list.length); + return list[randomIndex]; + }); + } else { + return input; + } +} + +function diceRollReplace(input, invalidRollPlaceholder = '') { + const rollPattern = /{{roll[ : ]([^}]+)}}/gi; + + return input.replace(rollPattern, (match, matchValue) => { + let formula = matchValue.trim(); + + if (isDigitsOnly(formula)) { + formula = `1d${formula}`; + } + + const isValid = droll.validate(formula); + + if (!isValid) { + console.debug(`Invalid roll formula: ${formula}`); + return invalidRollPlaceholder; + } + + const result = droll.roll(formula); + return new String(result.total); + }); +} + +/** + * Substitutes {{macro}} parameters in a string. + * @param {string} content - The string to substitute parameters in. + * @param {*} _name1 - The name of the user. + * @param {*} _name2 - The name of the character. + * @param {*} _original - The original message for {{original}} substitution. + * @param {*} _group - The group members list for {{group}} substitution. + * @returns {string} The string with substituted parameters. + */ +export function evaluateMacros(content, _name1, _name2, _original, _group, _replaceCharacterCard = true) { + if (!content) { + return ''; + } + + // Replace {{original}} with the original message + // Note: only replace the first instance of {{original}} + // This will hopefully prevent the abuse + if (typeof _original === 'string') { + content = content.replace(/{{original}}/i, _original); + } + content = diceRollReplace(content); + content = replaceInstructMacros(content); + content = replaceVariableMacros(content); + content = content.replace(/{{newline}}/gi, '\n'); + content = content.replace(/{{input}}/gi, String($('#send_textarea').val())); + + if (_replaceCharacterCard) { + const fields = getCharacterCardFields(); + content = content.replace(/{{charPrompt}}/gi, fields.system || ''); + content = content.replace(/{{charJailbreak}}/gi, fields.jailbreak || ''); + content = content.replace(/{{description}}/gi, fields.description || ''); + content = content.replace(/{{personality}}/gi, fields.personality || ''); + content = content.replace(/{{scenario}}/gi, fields.scenario || ''); + content = content.replace(/{{persona}}/gi, fields.persona || ''); + content = content.replace(/{{mesExamples}}/gi, fields.mesExamples || ''); + } + + content = content.replace(/{{maxPrompt}}/gi, () => String(getMaxContextSize())); + content = content.replace(/{{user}}/gi, _name1); + content = content.replace(/{{char}}/gi, _name2); + content = content.replace(/{{charIfNotGroup}}/gi, _group); + content = content.replace(/{{group}}/gi, _group); + content = content.replace(/{{lastMessage}}/gi, getLastMessage()); + content = content.replace(/{{lastMessageId}}/gi, getLastMessageId()); + content = content.replace(/{{firstIncludedMessageId}}/gi, getFirstIncludedMessageId()); + content = content.replace(/{{lastSwipeId}}/gi, getLastSwipeId()); + content = content.replace(/{{currentSwipeId}}/gi, getCurrentSwipeId()); + + content = content.replace(//gi, _name1); + content = content.replace(//gi, _name2); + content = content.replace(//gi, _group); + content = content.replace(//gi, _group); + + content = content.replace(/\{\{\/\/([\s\S]*?)\}\}/gm, ''); + + content = content.replace(/{{time}}/gi, moment().format('LT')); + content = content.replace(/{{date}}/gi, moment().format('LL')); + content = content.replace(/{{weekday}}/gi, moment().format('dddd')); + content = content.replace(/{{isotime}}/gi, moment().format('HH:mm')); + content = content.replace(/{{isodate}}/gi, moment().format('YYYY-MM-DD')); + + content = content.replace(/{{datetimeformat +([^}]*)}}/gi, (_, format) => { + const formattedTime = moment().format(format); + return formattedTime; + }); + content = content.replace(/{{idle_duration}}/gi, () => getTimeSinceLastMessage()); + content = content.replace(/{{time_UTC([-+]\d+)}}/gi, (_, offset) => { + const utcOffset = parseInt(offset, 10); + const utcTime = moment().utc().utcOffset(utcOffset).format('LT'); + return utcTime; + }); + content = bannedWordsReplace(content); + content = randomReplace(content); + return content; +} From 64783e73bd2b5d7dc702bba250ad7a7e3546c987 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 11 Jan 2024 15:03:55 +0200 Subject: [PATCH 163/522] Add prompt to reload page on extension update --- public/scripts/extensions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index f4bfc554c..125140b6d 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -654,7 +654,7 @@ async function updateExtension(extensionName, quiet) { toastr.success('Extension is already up to date'); } } else { - toastr.success(`Extension ${extensionName} updated to ${data.shortCommitHash}`); + toastr.success(`Extension ${extensionName} updated to ${data.shortCommitHash}`, 'Reload the page to apply updates'); } } catch (error) { console.error('Error:', error); From 706acbd514ca198cbd3c2d06dfaddef7ef33a765 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 11 Jan 2024 15:59:00 +0200 Subject: [PATCH 164/522] MistralAI monkey patch --- src/endpoints/backends/chat-completions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/endpoints/backends/chat-completions.js b/src/endpoints/backends/chat-completions.js index b1bde2149..247f16612 100644 --- a/src/endpoints/backends/chat-completions.js +++ b/src/endpoints/backends/chat-completions.js @@ -483,7 +483,7 @@ async function sendMistralAIRequest(request, response) { 'top_p': request.body.top_p, 'max_tokens': request.body.max_tokens, 'stream': request.body.stream, - 'safe_mode': request.body.safe_mode, + //'safe_mode': request.body.safe_mode, 'random_seed': request.body.seed === -1 ? undefined : request.body.seed, }; From ce4c1b8d01de84a44095e85cb144534cd534e8bb Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 11 Jan 2024 15:59:00 +0200 Subject: [PATCH 165/522] MistralAI monkey patch --- src/endpoints/backends/chat-completions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/endpoints/backends/chat-completions.js b/src/endpoints/backends/chat-completions.js index 712ed4485..9a5672f8f 100644 --- a/src/endpoints/backends/chat-completions.js +++ b/src/endpoints/backends/chat-completions.js @@ -478,7 +478,7 @@ async function sendMistralAIRequest(request, response) { 'top_p': request.body.top_p, 'max_tokens': request.body.max_tokens, 'stream': request.body.stream, - 'safe_mode': request.body.safe_mode, + //'safe_mode': request.body.safe_mode, 'random_seed': request.body.seed === -1 ? undefined : request.body.seed, }; From 747a7824c09464e96e9b57ff875b110f1f95e717 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 11 Jan 2024 20:27:59 +0200 Subject: [PATCH 166/522] OpenRouter model dropdown facelift --- public/css/select2-overrides.css | 14 +++++++++++ public/scripts/openai.js | 42 +++++++++++++++++++++++++------- 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/public/css/select2-overrides.css b/public/css/select2-overrides.css index f3cc9356a..cdab229d8 100644 --- a/public/css/select2-overrides.css +++ b/public/css/select2-overrides.css @@ -107,6 +107,12 @@ position: relative; } +.select2-results .select2-results__option--group { + color: var(--SmartThemeBodyColor); + background-color: var(--SmartThemeBlurTintColor); + position: relative; +} + /* Customize the hovered option list item */ .select2-results .select2-results__option--highlighted.select2-results__option--selectable { color: var(--SmartThemeBodyColor); @@ -114,12 +120,20 @@ opacity: 1; } +.select2-results__option.select2-results__option--group::before { + display: none; +} + /* Customize the option list item */ .select2-results__option { padding-left: 30px; /* Add some padding to make room for the checkbox */ } +.select2-results .select2-results__option--group .select2-results__options--nested .select2-results__option { + padding-left: 2em; +} + /* Add the custom checkbox */ .select2-results__option::before { content: ''; diff --git a/public/scripts/openai.js b/public/scripts/openai.js index 6d354ef17..8ac693cb9 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -62,6 +62,7 @@ import { formatInstructModePrompt, formatInstructModeSystemPrompt, } from './instruct-mode.js'; +import { isMobile } from './RossAscends-mods.js'; export { openai_messages_count, @@ -1298,6 +1299,25 @@ function getChatCompletionModel() { } } +function getOpenRouterModelTemplate(option){ + const model = model_list.find(x => x.id === option?.element?.value); + + if (!option.id || !model) { + return option.text; + } + + let tokens_dollar = Number(1 / (1000 * model.pricing?.prompt)); + let tokens_rounded = (Math.round(tokens_dollar * 1000) / 1000).toFixed(0); + + const price = 0 === Number(model.pricing?.prompt) ? 'Free' : `${tokens_rounded}k t/$ `; + + return $((` +
+
${DOMPurify.sanitize(model.name)} | ${model.context_length} ctx | ${price}
+
+ `)); +} + function calculateOpenRouterCost() { if (oai_settings.chat_completion_source !== chat_completion_sources.OPENROUTER) { return; @@ -1321,7 +1341,7 @@ function calculateOpenRouterCost() { } function saveModelList(data) { - model_list = data.map((model) => ({ id: model.id, context_length: model.context_length, pricing: model.pricing, architecture: model.architecture })); + model_list = data.map((model) => ({ ...model })); model_list.sort((a, b) => a?.id && b?.id && a.id.localeCompare(b.id)); if (oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER) { @@ -1376,16 +1396,10 @@ function appendOpenRouterOptions(model_list, groupModels = false, sort = false) $('#model_openrouter_select').append($('
+

From 6fe17a1bed99ca8b7d8468f47bfdfa71cbe5c235 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Wed, 17 Jan 2024 20:32:25 +0000 Subject: [PATCH 209/522] queue all auto-executes until APP_READY --- .../scripts/extensions/quick-reply/index.js | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/public/scripts/extensions/quick-reply/index.js b/public/scripts/extensions/quick-reply/index.js index c0142978b..48d200ae3 100644 --- a/public/scripts/extensions/quick-reply/index.js +++ b/public/scripts/extensions/quick-reply/index.js @@ -58,6 +58,10 @@ const defaultSettings = { }; +/** @type {Boolean}*/ +let isReady = false; +/** @type {Function[]}*/ +let executeQueue = []; /** @type {QuickReplySettings}*/ let settings; /** @type {SettingsUi} */ @@ -144,6 +148,16 @@ const loadSettings = async () => { } }; +const executeIfReadyElseQueue = async (functionToCall, args) => { + if (isReady) { + log('calling', { functionToCall, args }); + await functionToCall(...args); + } else { + log('queueing', { functionToCall, args }); + executeQueue.push(async()=>await functionToCall(...args)); + } +}; + @@ -183,9 +197,20 @@ const init = async () => { slash.init(); autoExec = new AutoExecuteHandler(settings); + log('executing startup'); await autoExec.handleStartup(); + log('/executing startup'); + + log(`executing queue (${executeQueue.length} items)`); + while (executeQueue.length > 0) { + const func = executeQueue.shift(); + await func(); + } + log('/executing queue'); + isReady = true; + log('READY'); }; -await init(); +init(); const onChatChanged = async (chatIdx) => { log('CHAT_CHANGED', chatIdx); @@ -199,14 +224,14 @@ const onChatChanged = async (chatIdx) => { await autoExec.handleChatChanged(); }; -eventSource.on(event_types.CHAT_CHANGED, onChatChanged); +eventSource.on(event_types.CHAT_CHANGED, (...args)=>executeIfReadyElseQueue(onChatChanged, args)); const onUserMessage = async () => { await autoExec.handleUser(); }; -eventSource.on(event_types.USER_MESSAGE_RENDERED, onUserMessage); +eventSource.on(event_types.USER_MESSAGE_RENDERED, (...args)=>executeIfReadyElseQueue(onUserMessage, args)); const onAiMessage = async () => { await autoExec.handleAi(); }; -eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, onAiMessage); +eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, (...args)=>executeIfReadyElseQueue(onAiMessage, args)); From 12a40c25a07c03cfc5203690dbc14b59d32ca58b Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Wed, 17 Jan 2024 20:41:59 +0000 Subject: [PATCH 210/522] fix QR settings UI out of sync after update via API --- public/scripts/extensions/quick-reply/api/QuickReplyApi.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/scripts/extensions/quick-reply/api/QuickReplyApi.js b/public/scripts/extensions/quick-reply/api/QuickReplyApi.js index 712b10d58..ddb12c29e 100644 --- a/public/scripts/extensions/quick-reply/api/QuickReplyApi.js +++ b/public/scripts/extensions/quick-reply/api/QuickReplyApi.js @@ -248,9 +248,9 @@ export class QuickReplyApi { if (!qr) { throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`); } - qr.label = newLabel ?? qr.label; - qr.message = message ?? qr.message; - qr.title = title ?? qr.title; + qr.updateLabel(newLabel ?? qr.label); + qr.updateMessage(message ?? qr.message); + qr.updateTitle(title ?? qr.title); qr.isHidden = isHidden ?? qr.isHidden; qr.executeOnStartup = executeOnStartup ?? qr.executeOnStartup; qr.executeOnUser = executeOnUser ?? qr.executeOnUser; From 3af2164187c13d817623125f9785afd76e0761c3 Mon Sep 17 00:00:00 2001 From: erew123 <35898566+erew123@users.noreply.github.com> Date: Wed, 17 Jan 2024 21:55:24 +0000 Subject: [PATCH 211/522] AllTalk Updates Streaming passed URL to global ST audio. Localstorage removed for saving TTS elements. Styles stored in CSS Duplicate checks on fetchresponse removed. --- public/scripts/extensions/tts/alltalk.js | 315 ++-- public/scripts/extensions/tts/index.js | 2094 +++++++++++----------- public/scripts/extensions/tts/style.css | 69 + 3 files changed, 1220 insertions(+), 1258 deletions(-) diff --git a/public/scripts/extensions/tts/alltalk.js b/public/scripts/extensions/tts/alltalk.js index e44b31240..f70daf5ad 100644 --- a/public/scripts/extensions/tts/alltalk.js +++ b/public/scripts/extensions/tts/alltalk.js @@ -3,92 +3,33 @@ import { saveTtsProviderSettings } from './index.js'; export { AllTalkTtsProvider }; -const css = ` -.settings-row { - display: flex; - justify-content: space-between; /* Distribute space evenly between the children */ - align-items: center; - width: 100%; /* Full width to spread out the children */ -} - -.settings-option { - flex: 1; /* Each child will take equal space */ - margin: 0 10px; /* Adjust spacing as needed */ -} - -.endpoint-option { - flex: 1; /* Each child will take equal space */ - margin: 0 10px; /* Adjust spacing as needed */ - margin-right: 25px; - width: 38%; /* Full width to spread out the children */ -} - -.website-row { - display: flex; - justify-content: start; /* Aligns items to the start of the row */ - align-items: center; - margin-top: 10px; /* Top margin */ - margin-bottom: 10px; /* Bottom margin */ -} - -.website-option { - flex: 1; /* Allows each option to take equal space */ - margin-right: 10px; /* Spacing between the two options, adjust as needed */ - margin-left: 10px; /* Adjust this value to align as desired */ - /* Remove the individual margin top and bottom declarations if they are set globally on the .settings-row */ -} - -.settings-separator { - margin-top: 10px; - margin-bottom: 10px; - padding: 18x; - font-weight: bold; - border-top: 1px solid #e1e1e1; /* Grey line */ - border-bottom: 1px solid #e1e1e1; /* Grey line */ - text-align: center; -} - -.status-message { - flex: 1; /* Adjust as needed */ - margin: 0 10px; - /* Additional styling */ -} - -.model-endpoint-row { - display: flex; - justify-content: space-between; - align-items: center; - width: 100%; -} - -.model-option, .endpoint-option { - flex: 1; - margin: 0 10px; - margin-left: 10px; /* Adjust this value to align as desired */ - /* Adjust width as needed to fit in one line */ -} - -.endpoint-option { - /* Adjust width to align with model dropdown */ - width: 38%; -} - -#status_info { - color: lightgreen; /* Or any specific shade of green you prefer */ -} -`; - -const style = document.createElement('style'); -style.type = 'text/css'; -style.appendChild(document.createTextNode(css)); -document.head.appendChild(style); - class AllTalkTtsProvider { //########// // Config // //########// - settings; + settings = {}; + constructor() { + // Initialize with default settings if they are not already set + this.settings = { + provider_endpoint: this.settings.provider_endpoint || 'http://localhost:7851', + language: this.settings.language || 'en', + voiceMap: this.settings.voiceMap || {}, + at_generation_method: this.settings.at_generation_method || 'standard_generation', + narrator_enabled: this.settings.narrator_enabled || 'false', + at_narrator_text_not_inside: this.settings.at_narrator_text_not_inside || 'narrator', + narrator_voice_gen: this.settings.narrator_voice_gen || 'female_01.wav', + finetuned_model: this.settings.finetuned_model || 'false' + }; + // Separate property for dynamically updated settings from the server + this.dynamicSettings = { + modelsAvailable: [], + currentModel: '', + deepspeed_available: false, + deepSpeedEnabled: false, + lowVramEnabled: false, + }; + } ready = false; voices = []; separator = '. '; @@ -114,23 +55,12 @@ class AllTalkTtsProvider { 'Hindi': 'hi', }; - Settings = { - provider_endpoint: 'http://localhost:7851', - language: 'en', - voiceMap: {}, - at_generation_method: 'standard_generation', - narrator_enabled: 'false', - at_narrator_text_not_inside: 'narrator', - narrator_voice_gen: 'female_01.wav', - finetuned_model: 'false' - }; - get settingsHtml() { - let html = `
AllTalk Settings
`; + let html = `
AllTalk Settings
`; - html += `
+ html += `
-
+
@@ -149,7 +79,7 @@ class AllTalkTtsProvider {
-
+
`; if (this.voices) { @@ -170,7 +100,7 @@ class AllTalkTtsProvider { } html += `
-
+
@@ -191,49 +121,47 @@ class AllTalkTtsProvider {
-
+
- +
`; - html += `
-
+ html += `
+
-
+
-
+
Status: Ready
-
+
`; - html += `
-
- AllTalk Config & Docs. + html += `
+
+ AllTalk Config & Docs.
-
`; -html += `
-
+ html += `
+
Text-generation-webui users - Uncheck Enable TTS in Text-generation-webui.
`; - - return html; } @@ -244,14 +172,35 @@ html += `
async loadSettings(settings) { updateStatus('Offline'); - // Use default settings if no settings are provided - this.settings = Object.keys(settings).length === 0 ? this.Settings : settings; + if (Object.keys(settings).length === 0) { + console.info('Using default AllTalk TTS Provider settings'); + } else { + // Populate settings with provided values, ignoring server-provided settings + for (const key in settings) { + if (key in this.settings) { + this.settings[key] = settings[key]; + } else { + console.debug(`Ignoring non-user-configurable setting: ${key}`); + } + } + } + + // Update UI elements to reflect the loaded settings + $('#at_server').val(this.settings.provider_endpoint); + $('#language_options').val(this.settings.language); + //$('#voicemap').val(this.settings.voiceMap); + $('#at_generation_method').val(this.settings.at_generation_method); + $('#at_narrator_enabled').val(this.settings.narrator_enabled); + $('#at_narrator_text_not_inside').val(this.settings.at_narrator_text_not_inside); + $('#narrator_voice').val(this.settings.narrator_voice_gen); + + console.debug('AllTalkTTS: Settings loaded'); try { // Check if TTS provider is ready await this.checkReady(); + await this.updateSettingsFromServer(); // Fetch dynamic settings from the TTS server await this.fetchTtsVoiceObjects(); // Fetch voices only if service is ready - await this.updateSettingsFromServer(); this.updateNarratorVoicesDropdown(); this.updateLanguageDropdown(); this.setupEventListeners(); @@ -259,29 +208,23 @@ html += `
updateStatus('Ready'); } catch (error) { console.error("Error loading settings:", error); - // Set status to Error if there is an issue in loading settings updateStatus('Offline'); } } applySettingsToHTML() { - // Load AllTalk specific settings first - const loadedSettings = loadAllTalkSettings(); // Apply loaded settings or use defaults - this.settings = loadedSettings ? { ...this.Settings, ...loadedSettings } : this.Settings; const narratorVoiceSelect = document.getElementById('narrator_voice'); const atNarratorSelect = document.getElementById('at_narrator_enabled'); const textNotInsideSelect = document.getElementById('at_narrator_text_not_inside'); const generationMethodSelect = document.getElementById('at_generation_method'); + this.settings.narrator_voice = this.settings.narrator_voice_gen; // Apply settings to Narrator Voice dropdown if (narratorVoiceSelect && this.settings.narrator_voice) { narratorVoiceSelect.value = this.settings.narrator_voice.replace('.wav', ''); - this.settings.narrator_voice_gen = this.settings.narrator_voice; - //console.log(this.settings.narrator_voice_gen) } // Apply settings to AT Narrator Enabled dropdown if (atNarratorSelect) { - //console.log(this.settings.narrator_enabled) // Sync the state with the checkbox in index.js const ttsPassAsterisksCheckbox = document.getElementById('tts_pass_asterisks'); // Access the checkbox from index.js const ttsNarrateQuotedCheckbox = document.getElementById('tts_narrate_quoted'); // Access the checkbox from index.js @@ -314,18 +257,15 @@ html += `
const languageSelect = document.getElementById('language_options'); if (languageSelect && this.settings.language) { languageSelect.value = this.settings.language; - //console.log(this.settings.language) } // Apply settings to Text Not Inside dropdown if (textNotInsideSelect && this.settings.text_not_inside) { textNotInsideSelect.value = this.settings.text_not_inside; this.settings.at_narrator_text_not_inside = this.settings.text_not_inside; - //console.log(this.settings.at_narrator_text_not_inside) } // Apply settings to Generation Method dropdown if (generationMethodSelect && this.settings.at_generation_method) { generationMethodSelect.value = this.settings.at_generation_method; - //console.log(this.settings.at_generation_method) } // Additional logic to disable/enable dropdowns based on the selected generation method const isStreamingEnabled = this.settings.at_generation_method === 'streaming_enabled'; @@ -347,7 +287,6 @@ html += `
ftOption.textContent = 'XTTSv2 FT'; modelSelect.appendChild(ftOption); } - //console.log("Settings applied to HTML."); } //##############################// @@ -486,7 +425,7 @@ html += `
const languageSelect = document.getElementById('language_options'); if (languageSelect) { // Ensure default language is set - this.settings.language = this.Settings.language; + this.settings.language = this.settings.language; languageSelect.innerHTML = ''; for (let language in this.languageLabels) { @@ -610,7 +549,7 @@ html += `
if (narratorVoiceSelect) { narratorVoiceSelect.addEventListener('change', (event) => { this.settings.narrator_voice_gen = `${event.target.value}.wav`; - this.onSettingsChangeAllTalk(); // Save the settings after change + this.onSettingsChange(); // Save the settings after change }); } @@ -618,7 +557,7 @@ html += `
if (textNotInsideSelect) { textNotInsideSelect.addEventListener('change', (event) => { this.settings.text_not_inside = event.target.value; - this.onSettingsChangeAllTalk(); // Save the settings after change + this.onSettingsChange(); // Save the settings after change }); } @@ -656,12 +595,11 @@ html += `
$('#tts_narrate_dialogues').click(); $('#tts_narrate_dialogues').trigger('change'); } - this.onSettingsChangeAllTalk(); // Save the settings after change + this.onSettingsChange(); // Save the settings after change }); } - // Event Listener for AT Generation Method Dropdown const atGenerationMethodSelect = document.getElementById('at_generation_method'); const atNarratorEnabledSelect = document.getElementById('at_narrator_enabled'); @@ -680,7 +618,7 @@ html += `
atNarratorEnabledSelect.disabled = false; } this.settings.at_generation_method = selectedMethod; // Update the setting here - this.onSettingsChangeAllTalk(); // Save the settings after change + this.onSettingsChange(); // Save the settings after change }); } @@ -689,9 +627,19 @@ html += `
if (languageSelect) { languageSelect.addEventListener('change', (event) => { this.settings.language = event.target.value; - this.onSettingsChangeAllTalk(); // Save the settings after change + this.onSettingsChange(); // Save the settings after change }); } + + // Listener for AllTalk Endpoint Input + const atServerInput = document.getElementById('at_server'); + if (atServerInput) { + atServerInput.addEventListener('input', (event) => { + this.settings.provider_endpoint = event.target.value; + this.onSettingsChange(); // Save the settings after change + }); + } + } //#############################// @@ -699,30 +647,16 @@ html += `
//#############################// onSettingsChange() { - // Update settings that SillyTavern will save - this.settings.provider_endpoint = $('#at_server').val(); + // Update settings based on the UI elements + //this.settings.provider_endpoint = $('#at_server').val(); this.settings.language = $('#language_options').val(); - saveTtsProviderSettings(); // This function should save settings handled by SillyTavern - // Call the function to handle AllTalk specific settings - this.onSettingsChangeAllTalk(); // Save the settings after change - } - - onSettingsChangeAllTalk() { - // Update AllTalk specific settings and save to localStorage - this.settings.narrator_enabled = $('#at_narrator_enabled').val() === 'true'; - this.settings.narrator_voice = $('#narrator_voice').val() + '.wav'; // assuming you need to append .wav - this.settings.text_not_inside = $('#at_narrator_text_not_inside').val(); // "character" or "narrator" - this.settings.at_generation_method = $('#at_generation_method').val(); // Streaming or standard - this.settings.language = $('#language_options').val(); // Streaming or standard - - // Call the save function with the current settings - saveAllTalkSettings({ - narrator_voice: this.settings.narrator_voice, - narrator_enabled: this.settings.narrator_enabled, - text_not_inside: this.settings.text_not_inside, - at_generation_method: this.settings.at_generation_method, - language: this.settings.language - }); + //this.settings.voiceMap = $('#voicemap').val(); + this.settings.at_generation_method = $('#at_generation_method').val(); + this.settings.narrator_enabled = $('#at_narrator_enabled').val(); + this.settings.at_narrator_text_not_inside = $('#at_narrator_text_not_inside').val(); + this.settings.narrator_voice_gen = $('#narrator_voice').val(); + // Save the updated settings + saveTtsProviderSettings(); } //#########################// @@ -802,38 +736,20 @@ html += `
async generateTts(inputText, voiceId) { try { if (this.settings.at_generation_method === 'streaming_enabled') { - // For streaming method + // Construct the streaming URL const streamingUrl = `${this.settings.provider_endpoint}/api/tts-generate-streaming?text=${encodeURIComponent(inputText)}&voice=${encodeURIComponent(voiceId)}.wav&language=${encodeURIComponent(this.settings.language)}&output_file=stream_output.wav`; - const audioElement = new Audio(streamingUrl); - audioElement.play(); // Play the audio stream directly console.log("Streaming URL:", streamingUrl); - return new Response(null, { - status: 200, - statusText: "OK", - headers: { - "Content-Type": "audio/wav", - "Content-Location": streamingUrl - } - }); + + // Return the streaming URL directly + return streamingUrl; } else { // For standard method const outputUrl = await this.fetchTtsGeneration(inputText, voiceId); - // Fetch the audio data as a blob from the URL const audioResponse = await fetch(outputUrl); if (!audioResponse.ok) { throw new Error(`HTTP ${audioResponse.status}: Failed to fetch audio data`); } - const audioBlob = await audioResponse.blob(); - if (!audioBlob.type.startsWith('audio/')) { - throw new Error(`Invalid audio data format. Expecting audio/*, got ${audioBlob.type}`); - } - return new Response(audioBlob, { - status: 200, - statusText: "OK", - headers: { - "Content-Type": audioBlob.type - } - }); + return audioResponse; // Return the fetch response directly } } catch (error) { console.error("Error in generateTts:", error); @@ -841,6 +757,7 @@ html += `
} } + //####################// // Generate Standard // //####################// @@ -852,8 +769,8 @@ html += `
'text_filtering': "standard", 'character_voice_gen': voiceId + ".wav", 'narrator_enabled': this.settings.narrator_enabled, - 'narrator_voice_gen': this.settings.narrator_voice_gen, - 'text_not_inside': this.settings.text_not_inside, + 'narrator_voice_gen': this.settings.narrator_voice_gen + ".wav", + 'text_not_inside': this.settings.at_narrator_text_not_inside, 'language': this.settings.language, 'output_file_name': "st_output", 'output_file_timestamp': "true", @@ -877,7 +794,6 @@ html += `
if (!response.ok) { const errorText = await response.text(); console.error(`[fetchTtsGeneration] Error Response Text:`, errorText); - // Uncomment the following line if you have a UI element for displaying errors // toastr.error(response.statusText, 'TTS Generation Failed'); throw new Error(`HTTP ${response.status}: ${errorText}`); } @@ -912,27 +828,6 @@ function updateStatus(message) { case 'Error': statusElement.style.color = 'red'; break; - // Add more cases as needed } } -} - -//########################// -// Save/load AT Settings // -//########################// - -function saveAllTalkSettings(settingsToSave) { - // Save the specific settings to localStorage - console.log("settings", settingsToSave) - localStorage.setItem('AllTalkSettings', JSON.stringify(settingsToSave)); -} - -function loadAllTalkSettings() { - // Retrieve the settings from localStorage - const settings = localStorage.getItem('AllTalkSettings'); - // If settings exist, parse them back into an object - if (settings) { - return JSON.parse(settings); - } - return null; -} +} \ No newline at end of file diff --git a/public/scripts/extensions/tts/index.js b/public/scripts/extensions/tts/index.js index 11f8ed4ec..dd84a22b5 100644 --- a/public/scripts/extensions/tts/index.js +++ b/public/scripts/extensions/tts/index.js @@ -1,1048 +1,1046 @@ -import { callPopup, cancelTtsPlay, eventSource, event_types, name2, saveSettingsDebounced } from '../../../script.js'; -import { ModuleWorkerWrapper, doExtrasFetch, extension_settings, getApiUrl, getContext, modules } from '../../extensions.js'; -import { delay, escapeRegex, getBase64Async, getStringHash, onlyUnique } from '../../utils.js'; -import { EdgeTtsProvider } from './edge.js'; -import { ElevenLabsTtsProvider } from './elevenlabs.js'; -import { SileroTtsProvider } from './silerotts.js'; -import { CoquiTtsProvider } from './coqui.js'; -import { SystemTtsProvider } from './system.js'; -import { NovelTtsProvider } from './novel.js'; -import { power_user } from '../../power-user.js'; -import { registerSlashCommand } from '../../slash-commands.js'; -import { OpenAITtsProvider } from './openai.js'; -import { XTTSTtsProvider } from './xtts.js'; -import { AllTalkTtsProvider } from './alltalk.js'; -export { talkingAnimation }; - -const UPDATE_INTERVAL = 1000; - -let voiceMapEntries = []; -let voiceMap = {}; // {charName:voiceid, charName2:voiceid2} -let storedvalue = false; -let lastChatId = null; -let lastMessageHash = null; - -const DEFAULT_VOICE_MARKER = '[Default Voice]'; -const DISABLED_VOICE_MARKER = 'disabled'; - -export function getPreviewString(lang) { - const previewStrings = { - 'en-US': 'The quick brown fox jumps over the lazy dog', - 'en-GB': 'Sphinx of black quartz, judge my vow', - 'fr-FR': 'Portez ce vieux whisky au juge blond qui fume', - 'de-DE': 'Victor jagt zwölf Boxkämpfer quer über den großen Sylter Deich', - 'it-IT': 'Pranzo d\'acqua fa volti sghembi', - 'es-ES': 'Quiere la boca exhausta vid, kiwi, piña y fugaz jamón', - 'es-MX': 'Fabio me exige, sin tapujos, que añada cerveza al whisky', - 'ru-RU': 'В чащах юга жил бы цитрус? Да, но фальшивый экземпляр!', - 'pt-BR': 'Vejo xá gritando que fez show sem playback.', - 'pt-PR': 'Todo pajé vulgar faz boquinha sexy com kiwi.', - 'uk-UA': 'Фабрикуймо гідність, лящім їжею, ґав хапаймо, з\'єднавці чаш!', - 'pl-PL': 'Pchnąć w tę łódź jeża lub ośm skrzyń fig', - 'cs-CZ': 'Příliš žluťoučký kůň úpěl ďábelské ódy', - 'sk-SK': 'Vyhŕňme si rukávy a vyprážajme čínske ryžové cestoviny', - 'hu-HU': 'Árvíztűrő tükörfúrógép', - 'tr-TR': 'Pijamalı hasta yağız şoföre çabucak güvendi', - 'nl-NL': 'De waard heeft een kalfje en een pinkje opgegeten', - 'sv-SE': 'Yxskaftbud, ge vårbygd, zinkqvarn', - 'da-DK': 'Quizdeltagerne spiste jordbær med fløde, mens cirkusklovnen Walther spillede på xylofon', - 'ja-JP': 'いろはにほへと ちりぬるを わかよたれそ つねならむ うゐのおくやま けふこえて あさきゆめみし ゑひもせす', - 'ko-KR': '가나다라마바사아자차카타파하', - 'zh-CN': '我能吞下玻璃而不伤身体', - 'ro-RO': 'Muzicologă în bej vând whisky și tequila, preț fix', - 'bg-BG': 'Щъркелите се разпръснаха по цялото небе', - 'el-GR': 'Ταχίστη αλώπηξ βαφής ψημένη γη, δρασκελίζει υπέρ νωθρού κυνός', - 'fi-FI': 'Voi veljet, miksi juuri teille myin nämä vehkeet?', - 'he-IL': 'הקצינים צעקו: "כל הכבוד לצבא הצבאות!"', - 'id-ID': 'Jangkrik itu memang enak, apalagi kalau digoreng', - 'ms-MY': 'Muzik penyanyi wanita itu menggambarkan kehidupan yang penuh dengan duka nestapa', - 'th-TH': 'เป็นไงบ้างครับ ผมชอบกินข้าวผัดกระเพราหมูกรอบ', - 'vi-VN': 'Cô bé quàng khăn đỏ đang ngồi trên bãi cỏ xanh', - 'ar-SA': 'أَبْجَدِيَّة عَرَبِيَّة', - 'hi-IN': 'श्वेता ने श्वेता के श्वेते हाथों में श्वेता का श्वेता चावल पकड़ा', - }; - const fallbackPreview = 'Neque porro quisquam est qui dolorem ipsum quia dolor sit amet'; - - return previewStrings[lang] ?? fallbackPreview; -} - -let ttsProviders = { - ElevenLabs: ElevenLabsTtsProvider, - Silero: SileroTtsProvider, - XTTSv2: XTTSTtsProvider, - System: SystemTtsProvider, - Coqui: CoquiTtsProvider, - Edge: EdgeTtsProvider, - Novel: NovelTtsProvider, - OpenAI: OpenAITtsProvider, - AllTalk: AllTalkTtsProvider, -}; -let ttsProvider; -let ttsProviderName; - -let ttsLastMessage = null; - -async function onNarrateOneMessage() { - audioElement.src = '/sounds/silence.mp3'; - const context = getContext(); - const id = $(this).closest('.mes').attr('mesid'); - const message = context.chat[id]; - - if (!message) { - return; - } - - resetTtsPlayback(); - ttsJobQueue.push(message); - moduleWorker(); -} - -async function onNarrateText(args, text) { - if (!text) { - return; - } - - audioElement.src = '/sounds/silence.mp3'; - - // To load all characters in the voice map, set unrestricted to true - await initVoiceMap(true); - - const baseName = args?.voice || name2; - const name = (baseName === 'SillyTavern System' ? DEFAULT_VOICE_MARKER : baseName) || DEFAULT_VOICE_MARKER; - - const voiceMapEntry = voiceMap[name] === DEFAULT_VOICE_MARKER - ? voiceMap[DEFAULT_VOICE_MARKER] - : voiceMap[name]; - - if (!voiceMapEntry || voiceMapEntry === DISABLED_VOICE_MARKER) { - toastr.info(`Specified voice for ${name} was not found. Check the TTS extension settings.`); - return; - } - - resetTtsPlayback(); - ttsJobQueue.push({ mes: text, name: name }); - await moduleWorker(); - - // Return back to the chat voices - await initVoiceMap(false); -} - -async function moduleWorker() { - // Primarily determining when to add new chat to the TTS queue - const enabled = $('#tts_enabled').is(':checked'); - $('body').toggleClass('tts', enabled); - if (!enabled) { - return; - } - - const context = getContext(); - const chat = context.chat; - - processTtsQueue(); - processAudioJobQueue(); - updateUiAudioPlayState(); - - // Auto generation is disabled - if (extension_settings.tts.auto_generation == false) { - return; - } - - // no characters or group selected - if (!context.groupId && context.characterId === undefined) { - return; - } - - // Chat changed - if ( - context.chatId !== lastChatId - ) { - currentMessageNumber = context.chat.length ? context.chat.length : 0; - saveLastValues(); - - // Force to speak on the first message in the new chat - if (context.chat.length === 1) { - lastMessageHash = -1; - } - - return; - } - - // take the count of messages - let lastMessageNumber = context.chat.length ? context.chat.length : 0; - - // There's no new messages - let diff = lastMessageNumber - currentMessageNumber; - let hashNew = getStringHash((chat.length && chat[chat.length - 1].mes) ?? ''); - - // if messages got deleted, diff will be < 0 - if (diff < 0) { - // necessary actions will be taken by the onChatDeleted() handler - return; - } - - // if no new messages, or same message, or same message hash, do nothing - if (diff == 0 && hashNew === lastMessageHash) { - return; - } - - // If streaming, wait for streaming to finish before processing new messages - if (context.streamingProcessor && !context.streamingProcessor.isFinished) { - return; - } - - // clone message object, as things go haywire if message object is altered below (it's passed by reference) - const message = structuredClone(chat[chat.length - 1]); - - // if last message within current message, message got extended. only send diff to TTS. - if (ttsLastMessage !== null && message.mes.indexOf(ttsLastMessage) !== -1) { - let tmp = message.mes; - message.mes = message.mes.replace(ttsLastMessage, ''); - ttsLastMessage = tmp; - } else { - ttsLastMessage = message.mes; - } - - // We're currently swiping. Don't generate voice - if (!message || message.mes === '...' || message.mes === '') { - return; - } - - // Don't generate if message doesn't have a display text - if (extension_settings.tts.narrate_translated_only && !(message?.extra?.display_text)) { - return; - } - - // Don't generate if message is a user message and user message narration is disabled - if (message.is_user && !extension_settings.tts.narrate_user) { - return; - } - - // New messages, add new chat to history - lastMessageHash = hashNew; - currentMessageNumber = lastMessageNumber; - - console.debug( - `Adding message from ${message.name} for TTS processing: "${message.mes}"`, - ); - ttsJobQueue.push(message); -} - -function talkingAnimation(switchValue) { - if (!modules.includes('talkinghead')) { - console.debug('Talking Animation module not loaded'); - return; - } - - const apiUrl = getApiUrl(); - const animationType = switchValue ? 'start' : 'stop'; - - if (switchValue !== storedvalue) { - try { - console.log(animationType + ' Talking Animation'); - doExtrasFetch(`${apiUrl}/api/talkinghead/${animationType}_talking`); - storedvalue = switchValue; // Update the storedvalue to the current switchValue - } catch (error) { - // Handle the error here or simply ignore it to prevent logging - } - } - updateUiAudioPlayState(); -} - -function resetTtsPlayback() { - // Stop system TTS utterance - cancelTtsPlay(); - - // Clear currently processing jobs - currentTtsJob = null; - currentAudioJob = null; - - // Reset audio element - audioElement.currentTime = 0; - audioElement.src = ''; - - // Clear any queue items - ttsJobQueue.splice(0, ttsJobQueue.length); - audioJobQueue.splice(0, audioJobQueue.length); - - // Set audio ready to process again - audioQueueProcessorReady = true; -} - -function isTtsProcessing() { - let processing = false; - - // Check job queues - if (ttsJobQueue.length > 0 || audioJobQueue.length > 0) { - processing = true; - } - // Check current jobs - if (currentTtsJob != null || currentAudioJob != null) { - processing = true; - } - return processing; -} - -function debugTtsPlayback() { - console.log(JSON.stringify( - { - 'ttsProviderName': ttsProviderName, - 'voiceMap': voiceMap, - 'currentMessageNumber': currentMessageNumber, - 'audioPaused': audioPaused, - 'audioJobQueue': audioJobQueue, - 'currentAudioJob': currentAudioJob, - 'audioQueueProcessorReady': audioQueueProcessorReady, - 'ttsJobQueue': ttsJobQueue, - 'currentTtsJob': currentTtsJob, - 'ttsConfig': extension_settings.tts, - }, - )); -} -window.debugTtsPlayback = debugTtsPlayback; - -//##################// -// Audio Control // -//##################// - -let audioElement = new Audio(); -audioElement.id = 'tts_audio'; -audioElement.autoplay = true; - -let audioJobQueue = []; -let currentAudioJob; -let audioPaused = false; -let audioQueueProcessorReady = true; - -async function playAudioData(audioBlob) { - // Since current audio job can be cancelled, don't playback if it is null - if (currentAudioJob == null) { - console.log('Cancelled TTS playback because currentAudioJob was null'); - } - if (audioBlob instanceof Blob) { - const srcUrl = await getBase64Async(audioBlob); - audioElement.src = srcUrl; - } else if (typeof audioBlob === 'string') { - audioElement.src = audioBlob; - } else { - throw `TTS received invalid audio data type ${typeof audioBlob}`; - } - audioElement.addEventListener('ended', completeCurrentAudioJob); - audioElement.addEventListener('canplay', () => { - console.debug('Starting TTS playback'); - audioElement.play(); - }); -} - -window['tts_preview'] = function (id) { - const audio = document.getElementById(id); - - if (audio && !$(audio).data('disabled')) { - audio.play(); - } - else { - ttsProvider.previewTtsVoice(id); - } -}; - -async function onTtsVoicesClick() { - let popupText = ''; - - try { - const voiceIds = await ttsProvider.fetchTtsVoiceObjects(); - - for (const voice of voiceIds) { - popupText += ` -
- ${voice.lang || ''} - ${voice.name} - -
`; - if (voice.preview_url) { - popupText += ``; - } - } - } catch { - popupText = 'Could not load voices list. Check your API key.'; - } - - callPopup(popupText, 'text'); -} - -function updateUiAudioPlayState() { - if (extension_settings.tts.enabled == true) { - $('#ttsExtensionMenuItem').show(); - let img; - // Give user feedback that TTS is active by setting the stop icon if processing or playing - if (!audioElement.paused || isTtsProcessing()) { - img = 'fa-solid fa-stop-circle extensionsMenuExtensionButton'; - } else { - img = 'fa-solid fa-circle-play extensionsMenuExtensionButton'; - } - $('#tts_media_control').attr('class', img); - } else { - $('#ttsExtensionMenuItem').hide(); - } -} - -function onAudioControlClicked() { - audioElement.src = '/sounds/silence.mp3'; - let context = getContext(); - // Not pausing, doing a full stop to anything TTS is doing. Better UX as pause is not as useful - if (!audioElement.paused || isTtsProcessing()) { - resetTtsPlayback(); - talkingAnimation(false); - } else { - // Default play behavior if not processing or playing is to play the last message. - ttsJobQueue.push(context.chat[context.chat.length - 1]); - } - updateUiAudioPlayState(); -} - -function addAudioControl() { - - $('#extensionsMenu').prepend(` -
-
- TTS Playback -
`); - $('#ttsExtensionMenuItem').attr('title', 'TTS play/pause').on('click', onAudioControlClicked); - updateUiAudioPlayState(); -} - -function completeCurrentAudioJob() { - audioQueueProcessorReady = true; - currentAudioJob = null; - talkingAnimation(false); //stop lip animation - // updateUiPlayState(); -} - -/** - * Accepts an HTTP response containing audio/mpeg data, and puts the data as a Blob() on the queue for playback - * @param {Response} response - */ -async function addAudioJob(response) { - if (typeof response === 'string') { - audioJobQueue.push(response); - } else { - const audioData = await response.blob(); - if (!audioData.type.startsWith('audio/')) { - throw `TTS received HTTP response with invalid data format. Expecting audio/*, got ${audioData.type}`; - } - audioJobQueue.push(audioData); - } - console.debug('Pushed audio job to queue.'); -} - -async function processAudioJobQueue() { - // Nothing to do, audio not completed, or audio paused - stop processing. - if (audioJobQueue.length == 0 || !audioQueueProcessorReady || audioPaused) { - return; - } - try { - audioQueueProcessorReady = false; - currentAudioJob = audioJobQueue.shift(); - playAudioData(currentAudioJob); - talkingAnimation(true); - } catch (error) { - console.error(error); - audioQueueProcessorReady = true; - } -} - -//################// -// TTS Control // -//################// - -let ttsJobQueue = []; -let currentTtsJob; // Null if nothing is currently being processed -let currentMessageNumber = 0; - -function completeTtsJob() { - console.info(`Current TTS job for ${currentTtsJob?.name} completed.`); - currentTtsJob = null; -} - -function saveLastValues() { - const context = getContext(); - lastChatId = context.chatId; - lastMessageHash = getStringHash( - (context.chat.length && context.chat[context.chat.length - 1].mes) ?? '', - ); -} - -async function tts(text, voiceId, char) { - async function processResponse(response) { - // RVC injection - if (extension_settings.rvc.enabled && typeof window['rvcVoiceConversion'] === 'function') - response = await window['rvcVoiceConversion'](response, char, text); - - await addAudioJob(response); - } - - let response = await ttsProvider.generateTts(text, voiceId); - - // If async generator, process every chunk as it comes in - if (typeof response[Symbol.asyncIterator] === 'function') { - for await (const chunk of response) { - await processResponse(chunk); - } - } else { - await processResponse(response); - } - - completeTtsJob(); -} - -async function processTtsQueue() { - // Called each moduleWorker iteration to pull chat messages from queue - if (currentTtsJob || ttsJobQueue.length <= 0 || audioPaused) { - return; - } - - console.debug('New message found, running TTS'); - currentTtsJob = ttsJobQueue.shift(); - let text = extension_settings.tts.narrate_translated_only ? (currentTtsJob?.extra?.display_text || currentTtsJob.mes) : currentTtsJob.mes; - - if (extension_settings.tts.skip_codeblocks) { - text = text.replace(/^\s{4}.*$/gm, '').trim(); - text = text.replace(/```.*?```/gs, '').trim(); - } - - if (!extension_settings.tts.pass_asterisks) { - text = extension_settings.tts.narrate_dialogues_only - ? text.replace(/\*[^*]*?(\*|$)/g, '').trim() // remove asterisks content - : text.replaceAll('*', '').trim(); // remove just the asterisks - } - - if (extension_settings.tts.narrate_quoted_only) { - const special_quotes = /[“”]/g; // Extend this regex to include other special quotes - text = text.replace(special_quotes, '"'); - const matches = text.match(/".*?"/g); // Matches text inside double quotes, non-greedily - const partJoiner = (ttsProvider?.separator || ' ... '); - text = matches ? matches.join(partJoiner) : text; - } - - if (typeof ttsProvider?.processText === 'function') { - text = await ttsProvider.processText(text); - } - - // Collapse newlines and spaces into single space - text = text.replace(/\s+/g, ' ').trim(); - - console.log(`TTS: ${text}`); - const char = currentTtsJob.name; - - // Remove character name from start of the line if power user setting is disabled - if (char && !power_user.allow_name2_display) { - const escapedChar = escapeRegex(char); - text = text.replace(new RegExp(`^${escapedChar}:`, 'gm'), ''); - } - - try { - if (!text) { - console.warn('Got empty text in TTS queue job.'); - completeTtsJob(); - return; - } - - const voiceMapEntry = voiceMap[char] === DEFAULT_VOICE_MARKER ? voiceMap[DEFAULT_VOICE_MARKER] : voiceMap[char]; - - if (!voiceMapEntry || voiceMapEntry === DISABLED_VOICE_MARKER) { - throw `${char} not in voicemap. Configure character in extension settings voice map`; - } - const voice = await ttsProvider.getVoice(voiceMapEntry); - const voiceId = voice.voice_id; - if (voiceId == null) { - toastr.error(`Specified voice for ${char} was not found. Check the TTS extension settings.`); - throw `Unable to attain voiceId for ${char}`; - } - tts(text, voiceId, char); - } catch (error) { - console.error(error); - currentTtsJob = null; - } -} - -// Secret function for now -async function playFullConversation() { - const context = getContext(); - const chat = context.chat; - ttsJobQueue = chat; -} -window.playFullConversation = playFullConversation; - -//#############################// -// Extension UI and Settings // -//#############################// - -function loadSettings() { - if (Object.keys(extension_settings.tts).length === 0) { - Object.assign(extension_settings.tts, defaultSettings); - } - for (const key in defaultSettings) { - if (!(key in extension_settings.tts)) { - extension_settings.tts[key] = defaultSettings[key]; - } - } - $('#tts_provider').val(extension_settings.tts.currentProvider); - $('#tts_enabled').prop( - 'checked', - extension_settings.tts.enabled, - ); - $('#tts_narrate_dialogues').prop('checked', extension_settings.tts.narrate_dialogues_only); - $('#tts_narrate_quoted').prop('checked', extension_settings.tts.narrate_quoted_only); - $('#tts_auto_generation').prop('checked', extension_settings.tts.auto_generation); - $('#tts_narrate_translated_only').prop('checked', extension_settings.tts.narrate_translated_only); - $('#tts_narrate_user').prop('checked', extension_settings.tts.narrate_user); - $('#tts_pass_asterisks').prop('checked', extension_settings.tts.pass_asterisks); - $('body').toggleClass('tts', extension_settings.tts.enabled); -} - -const defaultSettings = { - voiceMap: '', - ttsEnabled: false, - currentProvider: 'ElevenLabs', - auto_generation: true, - narrate_user: false, -}; - -function setTtsStatus(status, success) { - $('#tts_status').text(status); - if (success) { - $('#tts_status').removeAttr('style'); - } else { - $('#tts_status').css('color', 'red'); - } -} - -function onRefreshClick() { - Promise.all([ - ttsProvider.onRefreshClick(), - // updateVoiceMap() - ]).then(() => { - extension_settings.tts[ttsProviderName] = ttsProvider.settings; - saveSettingsDebounced(); - setTtsStatus('Successfully applied settings', true); - console.info(`Saved settings ${ttsProviderName} ${JSON.stringify(ttsProvider.settings)}`); - initVoiceMap(); - updateVoiceMap(); - }).catch(error => { - console.error(error); - setTtsStatus(error, false); - }); -} - -function onEnableClick() { - extension_settings.tts.enabled = $('#tts_enabled').is( - ':checked', - ); - updateUiAudioPlayState(); - saveSettingsDebounced(); -} - - -function onAutoGenerationClick() { - extension_settings.tts.auto_generation = !!$('#tts_auto_generation').prop('checked'); - saveSettingsDebounced(); -} - - -function onNarrateDialoguesClick() { - extension_settings.tts.narrate_dialogues_only = !!$('#tts_narrate_dialogues').prop('checked'); - saveSettingsDebounced(); - console.log("setting narrate changed", extension_settings.tts.narrate_dialogues_only) -} - -function onNarrateUserClick() { - extension_settings.tts.narrate_user = !!$('#tts_narrate_user').prop('checked'); - saveSettingsDebounced(); -} - -function onNarrateQuotedClick() { - extension_settings.tts.narrate_quoted_only = !!$('#tts_narrate_quoted').prop('checked'); - saveSettingsDebounced(); - console.log("setting narrate quoted changed", extension_settings.tts.narrate_quoted_only) -} - - -function onNarrateTranslatedOnlyClick() { - extension_settings.tts.narrate_translated_only = !!$('#tts_narrate_translated_only').prop('checked'); - saveSettingsDebounced(); -} - -function onSkipCodeblocksClick() { - extension_settings.tts.skip_codeblocks = !!$('#tts_skip_codeblocks').prop('checked'); - saveSettingsDebounced(); -} - -function onPassAsterisksClick() { - extension_settings.tts.pass_asterisks = !!$('#tts_pass_asterisks').prop('checked'); - saveSettingsDebounced(); - console.log("setting pass asterisks", extension_settings.tts.pass_asterisks) -} - -//##############// -// TTS Provider // -//##############// - -async function loadTtsProvider(provider) { - //Clear the current config and add new config - $('#tts_provider_settings').html(''); - - if (!provider) { - return; - } - - // Init provider references - extension_settings.tts.currentProvider = provider; - ttsProviderName = provider; - ttsProvider = new ttsProviders[provider]; - - // Init provider settings - $('#tts_provider_settings').append(ttsProvider.settingsHtml); - if (!(ttsProviderName in extension_settings.tts)) { - console.warn(`Provider ${ttsProviderName} not in Extension Settings, initiatilizing provider in settings`); - extension_settings.tts[ttsProviderName] = {}; - } - await ttsProvider.loadSettings(extension_settings.tts[ttsProviderName]); - await initVoiceMap(); -} - -function onTtsProviderChange() { - const ttsProviderSelection = $('#tts_provider').val(); - extension_settings.tts.currentProvider = ttsProviderSelection; - loadTtsProvider(ttsProviderSelection); -} - -// Ensure that TTS provider settings are saved to extension settings. -export function saveTtsProviderSettings() { - extension_settings.tts[ttsProviderName] = ttsProvider.settings; - updateVoiceMap(); - saveSettingsDebounced(); - console.info(`Saved settings ${ttsProviderName} ${JSON.stringify(ttsProvider.settings)}`); -} - - -//###################// -// voiceMap Handling // -//###################// - -async function onChatChanged() { - await resetTtsPlayback(); - const voiceMapInit = initVoiceMap(); - await Promise.race([voiceMapInit, delay(1000)]); - ttsLastMessage = null; -} - -async function onChatDeleted() { - const context = getContext(); - - // update internal references to new last message - lastChatId = context.chatId; - currentMessageNumber = context.chat.length ? context.chat.length : 0; - - // compare against lastMessageHash. If it's the same, we did not delete the last chat item, so no need to reset tts queue - let messageHash = getStringHash((context.chat.length && context.chat[context.chat.length - 1].mes) ?? ''); - if (messageHash === lastMessageHash) { - return; - } - lastMessageHash = messageHash; - ttsLastMessage = (context.chat.length && context.chat[context.chat.length - 1].mes) ?? ''; - - // stop any tts playback since message might not exist anymore - await resetTtsPlayback(); -} - -/** - * Get characters in current chat - * @param {boolean} unrestricted - If true, will include all characters in voiceMapEntries, even if they are not in the current chat. - * @returns {string[]} - Array of character names - */ -function getCharacters(unrestricted) { - const context = getContext(); - - if (unrestricted) { - const names = context.characters.map(char => char.name); - names.unshift(DEFAULT_VOICE_MARKER); - return names.filter(onlyUnique); - } - - let characters = []; - if (context.groupId === null) { - // Single char chat - characters.push(DEFAULT_VOICE_MARKER); - characters.push(context.name1); - characters.push(context.name2); - } else { - // Group chat - characters.push(DEFAULT_VOICE_MARKER); - characters.push(context.name1); - const group = context.groups.find(group => context.groupId == group.id); - for (let member of group.members) { - const character = context.characters.find(char => char.avatar == member); - if (character) { - characters.push(character.name); - } - } - } - return characters.filter(onlyUnique); -} - -function sanitizeId(input) { - // Remove any non-alphanumeric characters except underscore (_) and hyphen (-) - let sanitized = input.replace(/[^a-zA-Z0-9-_]/g, ''); - - // Ensure first character is always a letter - if (!/^[a-zA-Z]/.test(sanitized)) { - sanitized = 'element_' + sanitized; - } - - return sanitized; -} - -function parseVoiceMap(voiceMapString) { - let parsedVoiceMap = {}; - for (const [charName, voiceId] of voiceMapString - .split(',') - .map(s => s.split(':'))) { - if (charName && voiceId) { - parsedVoiceMap[charName.trim()] = voiceId.trim(); - } - } - return parsedVoiceMap; -} - - - -/** - * Apply voiceMap based on current voiceMapEntries - */ -function updateVoiceMap() { - const tempVoiceMap = {}; - for (const voice of voiceMapEntries) { - if (voice.voiceId === null) { - continue; - } - tempVoiceMap[voice.name] = voice.voiceId; - } - if (Object.keys(tempVoiceMap).length !== 0) { - voiceMap = tempVoiceMap; - console.log(`Voicemap updated to ${JSON.stringify(voiceMap)}`); - } - if (!extension_settings.tts[ttsProviderName].voiceMap) { - extension_settings.tts[ttsProviderName].voiceMap = {}; - } - Object.assign(extension_settings.tts[ttsProviderName].voiceMap, voiceMap); - saveSettingsDebounced(); -} - -class VoiceMapEntry { - name; - voiceId; - selectElement; - constructor(name, voiceId = DEFAULT_VOICE_MARKER) { - this.name = name; - this.voiceId = voiceId; - this.selectElement = null; - } - - addUI(voiceIds) { - let sanitizedName = sanitizeId(this.name); - let defaultOption = this.name === DEFAULT_VOICE_MARKER ? - `` : - ``; - let template = ` -
- ${this.name} - -
- `; - $('#tts_voicemap_block').append(template); - - // Populate voice ID select list - for (const voiceId of voiceIds) { - const option = document.createElement('option'); - option.innerText = voiceId.name; - option.value = voiceId.name; - $(`#tts_voicemap_char_${sanitizedName}_voice`).append(option); - } - - this.selectElement = $(`#tts_voicemap_char_${sanitizedName}_voice`); - this.selectElement.on('change', args => this.onSelectChange(args)); - this.selectElement.val(this.voiceId); - } - - onSelectChange(args) { - this.voiceId = this.selectElement.find(':selected').val(); - updateVoiceMap(); - } -} - -/** - * Init voiceMapEntries for character select list. - * @param {boolean} unrestricted - If true, will include all characters in voiceMapEntries, even if they are not in the current chat. - */ -export async function initVoiceMap(unrestricted = false) { - // Gate initialization if not enabled or TTS Provider not ready. Prevents error popups. - const enabled = $('#tts_enabled').is(':checked'); - if (!enabled) { - return; - } - - // Keep errors inside extension UI rather than toastr. Toastr errors for TTS are annoying. - try { - await ttsProvider.checkReady(); - } catch (error) { - const message = `TTS Provider not ready. ${error}`; - setTtsStatus(message, false); - return; - } - - setTtsStatus('TTS Provider Loaded', true); - - // Clear existing voiceMap state - $('#tts_voicemap_block').empty(); - voiceMapEntries = []; - - // Get characters in current chat - const characters = getCharacters(unrestricted); - - // Get saved voicemap from provider settings, handling new and old representations - let voiceMapFromSettings = {}; - if ('voiceMap' in extension_settings.tts[ttsProviderName]) { - // Handle previous representation - if (typeof extension_settings.tts[ttsProviderName].voiceMap === 'string') { - voiceMapFromSettings = parseVoiceMap(extension_settings.tts[ttsProviderName].voiceMap); - // Handle new representation - } else if (typeof extension_settings.tts[ttsProviderName].voiceMap === 'object') { - voiceMapFromSettings = extension_settings.tts[ttsProviderName].voiceMap; - } - } - - // Get voiceIds from provider - let voiceIdsFromProvider; - try { - voiceIdsFromProvider = await ttsProvider.fetchTtsVoiceObjects(); - } - catch { - toastr.error('TTS Provider failed to return voice ids.'); - } - - // Build UI using VoiceMapEntry objects - for (const character of characters) { - if (character === 'SillyTavern System') { - continue; - } - // Check provider settings for voiceIds - let voiceId; - if (character in voiceMapFromSettings) { - voiceId = voiceMapFromSettings[character]; - } else if (character === DEFAULT_VOICE_MARKER) { - voiceId = DISABLED_VOICE_MARKER; - } else { - voiceId = DEFAULT_VOICE_MARKER; - } - const voiceMapEntry = new VoiceMapEntry(character, voiceId); - voiceMapEntry.addUI(voiceIdsFromProvider); - voiceMapEntries.push(voiceMapEntry); - } - updateVoiceMap(); -} - -$(document).ready(function () { - function addExtensionControls() { - const settingsHtml = ` -
-
-
- TTS -
-
-
-
-
- Select TTS Provider
-
- - -
-
- - - - - - - - -
-
-
-
-
-
-
- -
-
-
-
-
- `; - $('#extensions_settings').append(settingsHtml); - $('#tts_refresh').on('click', onRefreshClick); - $('#tts_enabled').on('click', onEnableClick); - $('#tts_narrate_dialogues').on('click', onNarrateDialoguesClick); - $('#tts_narrate_quoted').on('click', onNarrateQuotedClick); - $('#tts_narrate_translated_only').on('click', onNarrateTranslatedOnlyClick); - $('#tts_skip_codeblocks').on('click', onSkipCodeblocksClick); - $('#tts_pass_asterisks').on('click', onPassAsterisksClick); - $('#tts_auto_generation').on('click', onAutoGenerationClick); - $('#tts_narrate_user').on('click', onNarrateUserClick); - $('#tts_voices').on('click', onTtsVoicesClick); - for (const provider in ttsProviders) { - $('#tts_provider').append($('` : + ``; + let template = ` +
+ ${this.name} + +
+ `; + $('#tts_voicemap_block').append(template); + + // Populate voice ID select list + for (const voiceId of voiceIds) { + const option = document.createElement('option'); + option.innerText = voiceId.name; + option.value = voiceId.name; + $(`#tts_voicemap_char_${sanitizedName}_voice`).append(option); + } + + this.selectElement = $(`#tts_voicemap_char_${sanitizedName}_voice`); + this.selectElement.on('change', args => this.onSelectChange(args)); + this.selectElement.val(this.voiceId); + } + + onSelectChange(args) { + this.voiceId = this.selectElement.find(':selected').val(); + updateVoiceMap(); + } +} + +/** + * Init voiceMapEntries for character select list. + * @param {boolean} unrestricted - If true, will include all characters in voiceMapEntries, even if they are not in the current chat. + */ +export async function initVoiceMap(unrestricted = false) { + // Gate initialization if not enabled or TTS Provider not ready. Prevents error popups. + const enabled = $('#tts_enabled').is(':checked'); + if (!enabled) { + return; + } + + // Keep errors inside extension UI rather than toastr. Toastr errors for TTS are annoying. + try { + await ttsProvider.checkReady(); + } catch (error) { + const message = `TTS Provider not ready. ${error}`; + setTtsStatus(message, false); + return; + } + + setTtsStatus('TTS Provider Loaded', true); + + // Clear existing voiceMap state + $('#tts_voicemap_block').empty(); + voiceMapEntries = []; + + // Get characters in current chat + const characters = getCharacters(unrestricted); + + // Get saved voicemap from provider settings, handling new and old representations + let voiceMapFromSettings = {}; + if ('voiceMap' in extension_settings.tts[ttsProviderName]) { + // Handle previous representation + if (typeof extension_settings.tts[ttsProviderName].voiceMap === 'string') { + voiceMapFromSettings = parseVoiceMap(extension_settings.tts[ttsProviderName].voiceMap); + // Handle new representation + } else if (typeof extension_settings.tts[ttsProviderName].voiceMap === 'object') { + voiceMapFromSettings = extension_settings.tts[ttsProviderName].voiceMap; + } + } + + // Get voiceIds from provider + let voiceIdsFromProvider; + try { + voiceIdsFromProvider = await ttsProvider.fetchTtsVoiceObjects(); + } + catch { + toastr.error('TTS Provider failed to return voice ids.'); + } + + // Build UI using VoiceMapEntry objects + for (const character of characters) { + if (character === 'SillyTavern System') { + continue; + } + // Check provider settings for voiceIds + let voiceId; + if (character in voiceMapFromSettings) { + voiceId = voiceMapFromSettings[character]; + } else if (character === DEFAULT_VOICE_MARKER) { + voiceId = DISABLED_VOICE_MARKER; + } else { + voiceId = DEFAULT_VOICE_MARKER; + } + const voiceMapEntry = new VoiceMapEntry(character, voiceId); + voiceMapEntry.addUI(voiceIdsFromProvider); + voiceMapEntries.push(voiceMapEntry); + } + updateVoiceMap(); +} + +$(document).ready(function () { + function addExtensionControls() { + const settingsHtml = ` +
+
+
+ TTS +
+
+
+
+
+ Select TTS Provider
+
+ + +
+
+ + + + + + + + +
+
+
+
+
+
+
+ +
+
+
+
+
+ `; + $('#extensions_settings').append(settingsHtml); + $('#tts_refresh').on('click', onRefreshClick); + $('#tts_enabled').on('click', onEnableClick); + $('#tts_narrate_dialogues').on('click', onNarrateDialoguesClick); + $('#tts_narrate_quoted').on('click', onNarrateQuotedClick); + $('#tts_narrate_translated_only').on('click', onNarrateTranslatedOnlyClick); + $('#tts_skip_codeblocks').on('click', onSkipCodeblocksClick); + $('#tts_pass_asterisks').on('click', onPassAsterisksClick); + $('#tts_auto_generation').on('click', onAutoGenerationClick); + $('#tts_narrate_user').on('click', onNarrateUserClick); + $('#tts_voices').on('click', onTtsVoicesClick); + for (const provider in ttsProviders) { + $('#tts_provider').append($('` : - ``; - let template = ` -
- ${this.name} - -
- `; - $('#tts_voicemap_block').append(template); - - // Populate voice ID select list - for (const voiceId of voiceIds) { - const option = document.createElement('option'); - option.innerText = voiceId.name; - option.value = voiceId.name; - $(`#tts_voicemap_char_${sanitizedName}_voice`).append(option); - } - - this.selectElement = $(`#tts_voicemap_char_${sanitizedName}_voice`); - this.selectElement.on('change', args => this.onSelectChange(args)); - this.selectElement.val(this.voiceId); - } - - onSelectChange(args) { - this.voiceId = this.selectElement.find(':selected').val(); - updateVoiceMap(); - } -} - -/** - * Init voiceMapEntries for character select list. - * @param {boolean} unrestricted - If true, will include all characters in voiceMapEntries, even if they are not in the current chat. - */ -export async function initVoiceMap(unrestricted = false) { - // Gate initialization if not enabled or TTS Provider not ready. Prevents error popups. - const enabled = $('#tts_enabled').is(':checked'); - if (!enabled) { - return; - } - - // Keep errors inside extension UI rather than toastr. Toastr errors for TTS are annoying. - try { - await ttsProvider.checkReady(); - } catch (error) { - const message = `TTS Provider not ready. ${error}`; - setTtsStatus(message, false); - return; - } - - setTtsStatus('TTS Provider Loaded', true); - - // Clear existing voiceMap state - $('#tts_voicemap_block').empty(); - voiceMapEntries = []; - - // Get characters in current chat - const characters = getCharacters(unrestricted); - - // Get saved voicemap from provider settings, handling new and old representations - let voiceMapFromSettings = {}; - if ('voiceMap' in extension_settings.tts[ttsProviderName]) { - // Handle previous representation - if (typeof extension_settings.tts[ttsProviderName].voiceMap === 'string') { - voiceMapFromSettings = parseVoiceMap(extension_settings.tts[ttsProviderName].voiceMap); - // Handle new representation - } else if (typeof extension_settings.tts[ttsProviderName].voiceMap === 'object') { - voiceMapFromSettings = extension_settings.tts[ttsProviderName].voiceMap; - } - } - - // Get voiceIds from provider - let voiceIdsFromProvider; - try { - voiceIdsFromProvider = await ttsProvider.fetchTtsVoiceObjects(); - } - catch { - toastr.error('TTS Provider failed to return voice ids.'); - } - - // Build UI using VoiceMapEntry objects - for (const character of characters) { - if (character === 'SillyTavern System') { - continue; - } - // Check provider settings for voiceIds - let voiceId; - if (character in voiceMapFromSettings) { - voiceId = voiceMapFromSettings[character]; - } else if (character === DEFAULT_VOICE_MARKER) { - voiceId = DISABLED_VOICE_MARKER; - } else { - voiceId = DEFAULT_VOICE_MARKER; - } - const voiceMapEntry = new VoiceMapEntry(character, voiceId); - voiceMapEntry.addUI(voiceIdsFromProvider); - voiceMapEntries.push(voiceMapEntry); - } - updateVoiceMap(); -} - -$(document).ready(function () { - function addExtensionControls() { - const settingsHtml = ` -
-
-
- TTS -
-
-
-
-
- Select TTS Provider
-
- - -
-
- - - - - - - - -
-
-
-
-
-
-
- -
-
-
-
-
- `; - $('#extensions_settings').append(settingsHtml); - $('#tts_refresh').on('click', onRefreshClick); - $('#tts_enabled').on('click', onEnableClick); - $('#tts_narrate_dialogues').on('click', onNarrateDialoguesClick); - $('#tts_narrate_quoted').on('click', onNarrateQuotedClick); - $('#tts_narrate_translated_only').on('click', onNarrateTranslatedOnlyClick); - $('#tts_skip_codeblocks').on('click', onSkipCodeblocksClick); - $('#tts_pass_asterisks').on('click', onPassAsterisksClick); - $('#tts_auto_generation').on('click', onAutoGenerationClick); - $('#tts_narrate_user').on('click', onNarrateUserClick); - $('#tts_voices').on('click', onTtsVoicesClick); - for (const provider in ttsProviders) { - $('#tts_provider').append($('` : + ``; + let template = ` +
+ ${this.name} + +
+ `; + $('#tts_voicemap_block').append(template); + + // Populate voice ID select list + for (const voiceId of voiceIds) { + const option = document.createElement('option'); + option.innerText = voiceId.name; + option.value = voiceId.name; + $(`#tts_voicemap_char_${sanitizedName}_voice`).append(option); + } + + this.selectElement = $(`#tts_voicemap_char_${sanitizedName}_voice`); + this.selectElement.on('change', args => this.onSelectChange(args)); + this.selectElement.val(this.voiceId); + } + + onSelectChange(args) { + this.voiceId = this.selectElement.find(':selected').val(); + updateVoiceMap(); + } +} + +/** + * Init voiceMapEntries for character select list. + * @param {boolean} unrestricted - If true, will include all characters in voiceMapEntries, even if they are not in the current chat. + */ +export async function initVoiceMap(unrestricted = false) { + // Gate initialization if not enabled or TTS Provider not ready. Prevents error popups. + const enabled = $('#tts_enabled').is(':checked'); + if (!enabled) { + return; + } + + // Keep errors inside extension UI rather than toastr. Toastr errors for TTS are annoying. + try { + await ttsProvider.checkReady(); + } catch (error) { + const message = `TTS Provider not ready. ${error}`; + setTtsStatus(message, false); + return; + } + + setTtsStatus('TTS Provider Loaded', true); + + // Clear existing voiceMap state + $('#tts_voicemap_block').empty(); + voiceMapEntries = []; + + // Get characters in current chat + const characters = getCharacters(unrestricted); + + // Get saved voicemap from provider settings, handling new and old representations + let voiceMapFromSettings = {}; + if ('voiceMap' in extension_settings.tts[ttsProviderName]) { + // Handle previous representation + if (typeof extension_settings.tts[ttsProviderName].voiceMap === 'string') { + voiceMapFromSettings = parseVoiceMap(extension_settings.tts[ttsProviderName].voiceMap); + // Handle new representation + } else if (typeof extension_settings.tts[ttsProviderName].voiceMap === 'object') { + voiceMapFromSettings = extension_settings.tts[ttsProviderName].voiceMap; + } + } + + // Get voiceIds from provider + let voiceIdsFromProvider; + try { + voiceIdsFromProvider = await ttsProvider.fetchTtsVoiceObjects(); + } + catch { + toastr.error('TTS Provider failed to return voice ids.'); + } + + // Build UI using VoiceMapEntry objects + for (const character of characters) { + if (character === 'SillyTavern System') { + continue; + } + // Check provider settings for voiceIds + let voiceId; + if (character in voiceMapFromSettings) { + voiceId = voiceMapFromSettings[character]; + } else if (character === DEFAULT_VOICE_MARKER) { + voiceId = DISABLED_VOICE_MARKER; + } else { + voiceId = DEFAULT_VOICE_MARKER; + } + const voiceMapEntry = new VoiceMapEntry(character, voiceId); + voiceMapEntry.addUI(voiceIdsFromProvider); + voiceMapEntries.push(voiceMapEntry); + } + updateVoiceMap(); +} + +$(document).ready(function () { + function addExtensionControls() { + const settingsHtml = ` +
+
+
+ TTS +
+
+
+
+
+ Select TTS Provider
+
+ + +
+
+ + + + + + + + +
+
+
+
+
+
+
+ +
+
+
+
+
+ `; + $('#extensions_settings').append(settingsHtml); + $('#tts_refresh').on('click', onRefreshClick); + $('#tts_enabled').on('click', onEnableClick); + $('#tts_narrate_dialogues').on('click', onNarrateDialoguesClick); + $('#tts_narrate_quoted').on('click', onNarrateQuotedClick); + $('#tts_narrate_translated_only').on('click', onNarrateTranslatedOnlyClick); + $('#tts_skip_codeblocks').on('click', onSkipCodeblocksClick); + $('#tts_pass_asterisks').on('click', onPassAsterisksClick); + $('#tts_auto_generation').on('click', onAutoGenerationClick); + $('#tts_narrate_user').on('click', onNarrateUserClick); + $('#tts_voices').on('click', onTtsVoicesClick); + for (const provider in ttsProviders) { + $('#tts_provider').append($('
diff --git a/public/scripts/extensions/quick-reply/index.js b/public/scripts/extensions/quick-reply/index.js index 48d200ae3..af09a9c49 100644 --- a/public/scripts/extensions/quick-reply/index.js +++ b/public/scripts/extensions/quick-reply/index.js @@ -103,6 +103,7 @@ const loadSets = async () => { qr.executeOnUser = slot.autoExecute_userMessage ?? false; qr.executeOnAi = slot.autoExecute_botMessage ?? false; qr.executeOnChatChange = slot.autoExecute_chatLoad ?? false; + qr.executeOnGroupMemberDraft = slot.autoExecute_groupMemberDraft ?? false; qr.contextList = (slot.contextMenu ?? []).map(it=>({ set: it.preset, isChained: it.chain, @@ -235,3 +236,8 @@ const onAiMessage = async () => { await autoExec.handleAi(); }; eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, (...args)=>executeIfReadyElseQueue(onAiMessage, args)); + +const onGroupMemberDraft = async () => { + await autoExec.handleGroupMemberDraft(); +}; +eventSource.on(event_types.GROUP_MEMBER_DRAFTED, (...args) => executeIfReadyElseQueue(onGroupMemberDraft, args)); diff --git a/public/scripts/extensions/quick-reply/src/AutoExecuteHandler.js b/public/scripts/extensions/quick-reply/src/AutoExecuteHandler.js index 4d53b23dd..06656d51b 100644 --- a/public/scripts/extensions/quick-reply/src/AutoExecuteHandler.js +++ b/public/scripts/extensions/quick-reply/src/AutoExecuteHandler.js @@ -73,4 +73,13 @@ export class AutoExecuteHandler { ]; await this.performAutoExecute(qrList); } + + async handleGroupMemberDraft() { + if (!this.checkExecute()) return; + const qrList = [ + ...this.settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnGroupMemberDraft)).flat(), + ...(this.settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnGroupMemberDraft))?.flat() ?? []), + ]; + await this.performAutoExecute(qrList); + } } diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index e247aa940..a5ebb1f66 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -30,6 +30,7 @@ export class QuickReply { /**@type {Boolean}*/ executeOnUser = false; /**@type {Boolean}*/ executeOnAi = false; /**@type {Boolean}*/ executeOnChatChange = false; + /**@type {Boolean}*/ executeOnGroupMemberDraft = false; /**@type {Function}*/ onExecute; /**@type {Function}*/ onDelete; @@ -351,7 +352,13 @@ export class QuickReply { this.executeOnChatChange = executeOnChatChange.checked; this.updateContext(); }); - + /**@type {HTMLInputElement}*/ + const executeOnGroupMemberDraft = dom.querySelector('#qr--executeOnGroupMemberDraft'); + executeOnGroupMemberDraft.checked = this.executeOnGroupMemberDraft; + executeOnGroupMemberDraft.addEventListener('click', ()=>{ + this.executeOnGroupMemberDraft = executeOnGroupMemberDraft.checked; + this.updateContext(); + }); /**@type {HTMLElement}*/ const executeErrors = dom.querySelector('#qr--modal-executeErrors'); @@ -484,6 +491,7 @@ export class QuickReply { executeOnUser: this.executeOnUser, executeOnAi: this.executeOnAi, executeOnChatChange: this.executeOnChatChange, + executeOnGroupMemberDraft: this.executeOnGroupMemberDraft, }; } } diff --git a/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js b/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js index 857714c0c..9ad225091 100644 --- a/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js +++ b/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js @@ -36,6 +36,7 @@ export class SlashCommandHandler { user - bool - auto execute on user message, e.g., user=true bot - bool - auto execute on AI message, e.g., bot=true load - bool - auto execute on chat load, e.g., load=true + group - bool - auto execute on group member selection, e.g., group=true title - string - title / tooltip to be shown on button, e.g., title="My Fancy Button" `.trim(); const qrUpdateArgs = ` @@ -148,6 +149,7 @@ export class SlashCommandHandler { executeOnUser: isTrueBoolean(args.user), executeOnAi: isTrueBoolean(args.bot), executeOnChatChange: isTrueBoolean(args.load), + executeOnGroupMemberDraft: isTrueBoolean(args.group), }, ); } catch (ex) { @@ -168,6 +170,7 @@ export class SlashCommandHandler { executeOnUser: args.user === undefined ? undefined : isTrueBoolean(args.user), executeOnAi: args.bot === undefined ? undefined : isTrueBoolean(args.bot), executeOnChatChange: args.load === undefined ? undefined : isTrueBoolean(args.load), + executeOnGroupMemberDraft: args.group === undefined ? undefined : isTrueBoolean(args.group), }, ); } catch (ex) { diff --git a/public/scripts/group-chats.js b/public/scripts/group-chats.js index bfd49f7c8..d43322e0b 100644 --- a/public/scripts/group-chats.js +++ b/public/scripts/group-chats.js @@ -729,6 +729,7 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) { const generateType = type == 'swipe' || type == 'impersonate' || type == 'quiet' || type == 'continue' ? type : 'group_chat'; setCharacterId(chId); setCharacterName(characters[chId].name); + await eventSource.emit(event_types.GROUP_MEMBER_DRAFTED, chId); if (type !== 'swipe' && type !== 'impersonate' && !isStreamingEnabled()) { // update indicator and scroll down From 8874ffffc59c93c137bf1e28fbd7219266fec16b Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 18 Jan 2024 18:23:00 +0200 Subject: [PATCH 217/522] Adjust UI label. Group members are peacenik --- public/scripts/extensions/quick-reply/html/qrEditor.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/extensions/quick-reply/html/qrEditor.html b/public/scripts/extensions/quick-reply/html/qrEditor.html index d57ff565d..ad5304c22 100644 --- a/public/scripts/extensions/quick-reply/html/qrEditor.html +++ b/public/scripts/extensions/quick-reply/html/qrEditor.html @@ -68,7 +68,7 @@
From 65d9c944d8a89c0f2b7a2c632474c2ac4f3548cb Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Thu, 18 Jan 2024 20:43:31 +0000 Subject: [PATCH 218/522] await init and wait for APP_READY --- public/scripts/extensions/quick-reply/index.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/public/scripts/extensions/quick-reply/index.js b/public/scripts/extensions/quick-reply/index.js index af09a9c49..5cb636d79 100644 --- a/public/scripts/extensions/quick-reply/index.js +++ b/public/scripts/extensions/quick-reply/index.js @@ -198,6 +198,9 @@ const init = async () => { slash.init(); autoExec = new AutoExecuteHandler(settings); + eventSource.on(event_types.APP_READY, ()=>finalizeInit()); +}; +const finalizeInit = async () => { log('executing startup'); await autoExec.handleStartup(); log('/executing startup'); @@ -211,7 +214,7 @@ const init = async () => { isReady = true; log('READY'); }; -init(); +await init(); const onChatChanged = async (chatIdx) => { log('CHAT_CHANGED', chatIdx); From 9ce2771dad7494dbb078724ac476b20a62127447 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Thu, 18 Jan 2024 20:47:46 +0000 Subject: [PATCH 219/522] make finalizeInit blocking just to be sure --- public/scripts/extensions/quick-reply/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/extensions/quick-reply/index.js b/public/scripts/extensions/quick-reply/index.js index 5cb636d79..735337795 100644 --- a/public/scripts/extensions/quick-reply/index.js +++ b/public/scripts/extensions/quick-reply/index.js @@ -198,7 +198,7 @@ const init = async () => { slash.init(); autoExec = new AutoExecuteHandler(settings); - eventSource.on(event_types.APP_READY, ()=>finalizeInit()); + eventSource.on(event_types.APP_READY, async()=>await finalizeInit()); }; const finalizeInit = async () => { log('executing startup'); From 0b322c0e3d97b76bad7aee7109c239f4d4a04343 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 18 Jan 2024 23:55:09 +0200 Subject: [PATCH 220/522] Add repetition penalty control for OpenRouter --- public/index.html | 15 ++++++++++++++- public/scripts/openai.js | 14 ++++++++++++++ src/endpoints/backends/chat-completions.js | 4 ++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/public/index.html b/public/index.html index b28e5cb35..abdf53a5b 100644 --- a/public/index.html +++ b/public/index.html @@ -523,6 +523,19 @@
+
+
+ Repetition Penalty +
+
+
+ +
+
+ +
+
+
Min P @@ -5235,4 +5248,4 @@ - \ No newline at end of file + diff --git a/public/scripts/openai.js b/public/scripts/openai.js index 099b3d5f9..c16280491 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -190,6 +190,7 @@ const default_settings = { top_k_openai: 0, min_p_openai: 0, top_a_openai: 1, + repetition_penalty_openai: 1, stream_openai: false, openai_max_context: max_4k, openai_max_tokens: 300, @@ -257,6 +258,7 @@ const oai_settings = { top_k_openai: 0, min_p_openai: 0, top_a_openai: 1, + repetition_penalty_openai: 1, stream_openai: false, openai_max_context: max_4k, openai_max_tokens: 300, @@ -1605,6 +1607,7 @@ async function sendOpenAIRequest(type, messages, signal) { if (isOpenRouter) { generate_data['top_k'] = Number(oai_settings.top_k_openai); generate_data['min_p'] = Number(oai_settings.min_p_openai); + generate_data['repetition_penalty'] = Number(oai_settings.repetition_penalty_openai); generate_data['top_a'] = Number(oai_settings.top_a_openai); generate_data['use_fallback'] = oai_settings.openrouter_use_fallback; @@ -2369,6 +2372,7 @@ function loadOpenAISettings(data, settings) { oai_settings.top_k_openai = settings.top_k_openai ?? default_settings.top_k_openai; oai_settings.top_a_openai = settings.top_a_openai ?? default_settings.top_a_openai; oai_settings.min_p_openai = settings.min_p_openai ?? default_settings.min_p_openai; + oai_settings.repetition_penalty_openai = settings.repetition_penalty_openai ?? default_settings.repetition_penalty_openai; oai_settings.stream_openai = settings.stream_openai ?? default_settings.stream_openai; oai_settings.openai_max_context = settings.openai_max_context ?? default_settings.openai_max_context; oai_settings.openai_max_tokens = settings.openai_max_tokens ?? default_settings.openai_max_tokens; @@ -2504,6 +2508,8 @@ function loadOpenAISettings(data, settings) { $('#top_a_counter_openai').val(Number(oai_settings.top_a_openai)); $('#min_p_openai').val(oai_settings.min_p_openai); $('#min_p_counter_openai').val(Number(oai_settings.min_p_openai)); + $('#repetition_penalty_openai').val(oai_settings.repetition_penalty_openai); + $('#repetition_penalty_counter_openai').val(Number(oai_settings.repetition_penalty_openai)); $('#seed_openai').val(oai_settings.seed); if (settings.reverse_proxy !== undefined) oai_settings.reverse_proxy = settings.reverse_proxy; @@ -2650,6 +2656,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) { top_k: settings.top_k_openai, top_a: settings.top_a_openai, min_p: settings.min_p_openai, + repetition_penalty: settings.repetition_penalty_openai, openai_max_context: settings.openai_max_context, openai_max_tokens: settings.openai_max_tokens, wrap_in_quotes: settings.wrap_in_quotes, @@ -3011,6 +3018,7 @@ function onSettingsPresetChange() { top_k: ['#top_k_openai', 'top_k_openai', false], top_a: ['#top_a_openai', 'top_a_openai', false], min_p: ['#min_p_openai', 'min_p_openai', false], + repetition_penalty: ['#repetition_penalty_openai', 'repetition_penalty_openai', false], max_context_unlocked: ['#oai_max_context_unlocked', 'max_context_unlocked', true], openai_model: ['#model_openai_select', 'openai_model', false], claude_model: ['#model_claude_select', 'claude_model', false], @@ -3747,6 +3755,12 @@ $(document).ready(async function () { saveSettingsDebounced(); }); + $('#repetition_penalty_openai').on('input', function () { + oai_settings.repetition_penalty_openai = Number($(this).val()); + $('#repetition_penalty_counter_openai').val(Number($(this).val())); + saveSettingsDebounced(); + }); + $('#openai_max_context').on('input', function () { oai_settings.openai_max_context = Number($(this).val()); $('#openai_max_context_counter').val(`${$(this).val()}`); diff --git a/src/endpoints/backends/chat-completions.js b/src/endpoints/backends/chat-completions.js index cef8b906d..7fc294e04 100644 --- a/src/endpoints/backends/chat-completions.js +++ b/src/endpoints/backends/chat-completions.js @@ -730,6 +730,10 @@ router.post('/generate', jsonParser, function (request, response) { bodyParams['top_a'] = request.body.top_a; } + if (request.body.repetition_penalty !== undefined) { + bodyParams['repetition_penalty'] = request.body.repetition_penalty; + } + if (request.body.use_fallback) { bodyParams['route'] = 'fallback'; } From 49a5031e58ea053101e14947f07f51fae3761c83 Mon Sep 17 00:00:00 2001 From: Tony Ribeiro Date: Fri, 19 Jan 2024 09:29:49 +0100 Subject: [PATCH 221/522] Clean debug comments --- public/index.html | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/public/index.html b/public/index.html index 81d9d5551..09416f5f3 100644 --- a/public/index.html +++ b/public/index.html @@ -22,8 +22,7 @@ - - + - - From b741f32ae941bab1ccdb5c2791ac3c698debef3e Mon Sep 17 00:00:00 2001 From: Tony Ribeiro Date: Fri, 19 Jan 2024 09:34:32 +0100 Subject: [PATCH 222/522] Clean comments --- public/scripts/extensions/tts/index.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/public/scripts/extensions/tts/index.js b/public/scripts/extensions/tts/index.js index 36a80ecc5..d9143998a 100644 --- a/public/scripts/extensions/tts/index.js +++ b/public/scripts/extensions/tts/index.js @@ -321,10 +321,12 @@ async function playAudioData(audioJob) { } if (audioBlob instanceof Blob) { const srcUrl = await getBase64Async(audioBlob); - // VRM inject + + // VRM lip sync if (extension_settings.vrm.enabled && typeof window['vrmLipSync'] === 'function') { await window['vrmLipSync'](audioBlob, audioJob["char"]); } + audioElement.src = srcUrl; } else if (typeof audioBlob === 'string') { audioElement.src = audioBlob; @@ -481,11 +483,6 @@ async function tts(text, voiceId, char) { if (extension_settings.rvc.enabled && typeof window['rvcVoiceConversion'] === 'function') response = await window['rvcVoiceConversion'](response, char, text); - /*/ VRM injection - if (extension_settings.vrm.enabled && typeof window['vrmLipSync'] === 'function') { - await window['vrmLipSync'](response, char); - }*/ - await addAudioJob(response, char); } From bce5352c943014621408fbcaa0b0ef962ebfa6ea Mon Sep 17 00:00:00 2001 From: Tony Ribeiro Date: Fri, 19 Jan 2024 17:07:10 +0100 Subject: [PATCH 223/522] Removed VRM importmap. --- public/index.html | 9 --------- 1 file changed, 9 deletions(-) diff --git a/public/index.html b/public/index.html index 07af050ea..37e4971ed 100644 --- a/public/index.html +++ b/public/index.html @@ -22,15 +22,6 @@ - - From b7f46b1cdf545d7449ec8f2dbe67747f23d7eeb6 Mon Sep 17 00:00:00 2001 From: Tony Ribeiro Date: Fri, 19 Jan 2024 17:08:45 +0100 Subject: [PATCH 224/522] Remove typo --- public/index.html | 1 - 1 file changed, 1 deletion(-) diff --git a/public/index.html b/public/index.html index 37e4971ed..abdf53a5b 100644 --- a/public/index.html +++ b/public/index.html @@ -22,7 +22,6 @@ - From 2846d0fd58e19a30d1df869e25f740762e18ea21 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 20 Jan 2024 19:48:56 +0200 Subject: [PATCH 225/522] #1720 Fetch no-cache images when uploading --- .../scripts/extensions/expressions/index.js | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/public/scripts/extensions/expressions/index.js b/public/scripts/extensions/expressions/index.js index ee3e9b5a1..39f296545 100644 --- a/public/scripts/extensions/expressions/index.js +++ b/public/scripts/extensions/expressions/index.js @@ -1376,6 +1376,7 @@ async function handleFileUpload(url, formData) { // Refresh sprites list const name = formData.get('name'); delete spriteCache[name]; + await fetchImagesNoCache(); await validateImages(name); return data; @@ -1552,6 +1553,7 @@ async function onClickExpressionDelete(event) { // Refresh sprites list delete spriteCache[name]; + await fetchImagesNoCache(); await validateImages(name); } @@ -1577,6 +1579,30 @@ function setExpressionOverrideHtml(forceClear = false) { } } +async function fetchImagesNoCache() { + const promises = []; + $('#image_list img').each(function () { + const src = $(this).attr('src'); + + if (!src) { + return; + } + + const promise = fetch(src, { + method: 'GET', + cache: 'no-cache', + headers: { + 'Cache-Control': 'no-cache', + 'Pragma': 'no-cache', + 'Expires': '0', + }, + }); + promises.push(promise); + }); + + return await Promise.allSettled(promises); +} + (function () { function addExpressionImage() { const html = ` From 67c8970373b206d9a97edf323fec1a48dc042e84 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 20 Jan 2024 19:51:08 +0200 Subject: [PATCH 226/522] #1719 Hide HTML formulas --- public/css/st-tailwind.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/css/st-tailwind.css b/public/css/st-tailwind.css index 015cf6f51..2472b1a8f 100644 --- a/public/css/st-tailwind.css +++ b/public/css/st-tailwind.css @@ -432,6 +432,7 @@ line-height: 1.2; } +.custom-katex-html, .katex-html { display: none; } @@ -530,4 +531,4 @@ textarea:disabled { height: 30px; text-align: center; padding: 5px; -} \ No newline at end of file +} From 3cb94135412d4f6dad433c3fd25252a008a3fdd9 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 20 Jan 2024 20:13:41 +0200 Subject: [PATCH 227/522] #1718 Fix message search opening wrong chats --- public/script.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/public/script.js b/public/script.js index b34c88d58..3e491540a 100644 --- a/public/script.js +++ b/public/script.js @@ -5866,6 +5866,7 @@ export async function getPastCharacterChats(characterId = null) { */ export async function displayPastChats() { $('#select_chat_div').empty(); + $('#select_chat_search').val('').off('input'); const group = selected_group ? groups.find(x => x.id === selected_group) : null; const data = await (selected_group ? getGroupPastChats(selected_group) : getPastCharacterChats()); @@ -5895,25 +5896,25 @@ export async function displayPastChats() { return chatContent && Object.values(chatContent).some(message => message?.mes?.toLowerCase()?.includes(searchQuery.toLowerCase())); }); - console.log(filteredData); - for (const key in filteredData) { + console.debug(filteredData); + for (const value of filteredData.values()) { let strlen = 300; - let mes = filteredData[key]['mes']; + let mes = value['mes']; if (mes !== undefined) { if (mes.length > strlen) { mes = '...' + mes.substring(mes.length - strlen); } - const chat_items = data[key]['chat_items']; - const file_size = data[key]['file_size']; - const fileName = data[key]['file_name']; - const timestamp = timestampToMoment(data[key]['last_mes']).format('lll'); + const fileSize = value['file_size']; + const fileName = value['file_name']; + const chatItems = rawChats[fileName].length; + const timestamp = timestampToMoment(value['last_mes']).format('lll'); const template = $('#past_chat_template .select_chat_block_wrapper').clone(); template.find('.select_chat_block').attr('file_name', fileName); template.find('.avatar img').attr('src', avatarImg); template.find('.select_chat_block_filename').text(fileName); - template.find('.chat_file_size').text(`(${file_size},`); - template.find('.chat_messages_num').text(`${chat_items}💬)`); + template.find('.chat_file_size').text(`(${fileSize},`); + template.find('.chat_messages_num').text(`${chatItems}💬)`); template.find('.select_chat_block_mes').text(mes); template.find('.PastChat_cross').attr('file_name', fileName); template.find('.chat_messages_date').text(timestamp); @@ -8380,7 +8381,7 @@ jQuery(async function () { if (id == 'option_select_chat') { if ((selected_group && !is_group_generating) || (this_chid !== undefined && !is_send_press) || fromSlashCommand) { - displayPastChats(); + await displayPastChats(); //this is just to avoid the shadow for past chat view when using /delchat //however, the dialog popup still gets one.. if (!fromSlashCommand) { From 570d5a30bda7103ac92cef7087b9c5f7760ab6d1 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 20 Jan 2024 20:40:40 +0200 Subject: [PATCH 228/522] [skip ci] Lint fix --- public/scripts/extensions/tts/alltalk.js | 1666 +++++++++++----------- public/scripts/extensions/tts/index.js | 2 +- public/scripts/textgen-settings.js | 2 +- 3 files changed, 835 insertions(+), 835 deletions(-) diff --git a/public/scripts/extensions/tts/alltalk.js b/public/scripts/extensions/tts/alltalk.js index f70daf5ad..fe48043f0 100644 --- a/public/scripts/extensions/tts/alltalk.js +++ b/public/scripts/extensions/tts/alltalk.js @@ -1,833 +1,833 @@ -import { doExtrasFetch, getApiUrl, modules } from '../../extensions.js'; -import { saveTtsProviderSettings } from './index.js'; - -export { AllTalkTtsProvider }; - -class AllTalkTtsProvider { - //########// - // Config // - //########// - - settings = {}; - constructor() { - // Initialize with default settings if they are not already set - this.settings = { - provider_endpoint: this.settings.provider_endpoint || 'http://localhost:7851', - language: this.settings.language || 'en', - voiceMap: this.settings.voiceMap || {}, - at_generation_method: this.settings.at_generation_method || 'standard_generation', - narrator_enabled: this.settings.narrator_enabled || 'false', - at_narrator_text_not_inside: this.settings.at_narrator_text_not_inside || 'narrator', - narrator_voice_gen: this.settings.narrator_voice_gen || 'female_01.wav', - finetuned_model: this.settings.finetuned_model || 'false' - }; - // Separate property for dynamically updated settings from the server - this.dynamicSettings = { - modelsAvailable: [], - currentModel: '', - deepspeed_available: false, - deepSpeedEnabled: false, - lowVramEnabled: false, - }; - } - ready = false; - voices = []; - separator = '. '; - audioElement = document.createElement('audio'); - - languageLabels = { - 'Arabic': 'ar', - 'Brazilian Portuguese': 'pt', - 'Chinese': 'zh-cn', - 'Czech': 'cs', - 'Dutch': 'nl', - 'English': 'en', - 'French': 'fr', - 'German': 'de', - 'Italian': 'it', - 'Polish': 'pl', - 'Russian': 'ru', - 'Spanish': 'es', - 'Turkish': 'tr', - 'Japanese': 'ja', - 'Korean': 'ko', - 'Hungarian': 'hu', - 'Hindi': 'hi', - }; - - get settingsHtml() { - let html = `
AllTalk Settings
`; - - html += `
- -
- - -
-
`; - - html += `
- -
- - -
- -
- - -
- -
`; - - html += `
-
- - -
-
- - -
-
`; - - - html += `
-
- - -
- -
- - -
-
`; - - - html += `
-
- - -
-
- - -
-
- Status: Ready -
-
- -
-
`; - - - html += `
-
- AllTalk Config & Docs. -
- -
- AllTalk Website. -
-
`; - - html += `
-
-Text-generation-webui users - Uncheck Enable TTS in Text-generation-webui. -
-
`; - - return html; - } - - - //#################// - // Startup ST & AT // - //#################// - - async loadSettings(settings) { - updateStatus('Offline'); - - if (Object.keys(settings).length === 0) { - console.info('Using default AllTalk TTS Provider settings'); - } else { - // Populate settings with provided values, ignoring server-provided settings - for (const key in settings) { - if (key in this.settings) { - this.settings[key] = settings[key]; - } else { - console.debug(`Ignoring non-user-configurable setting: ${key}`); - } - } - } - - // Update UI elements to reflect the loaded settings - $('#at_server').val(this.settings.provider_endpoint); - $('#language_options').val(this.settings.language); - //$('#voicemap').val(this.settings.voiceMap); - $('#at_generation_method').val(this.settings.at_generation_method); - $('#at_narrator_enabled').val(this.settings.narrator_enabled); - $('#at_narrator_text_not_inside').val(this.settings.at_narrator_text_not_inside); - $('#narrator_voice').val(this.settings.narrator_voice_gen); - - console.debug('AllTalkTTS: Settings loaded'); - try { - // Check if TTS provider is ready - await this.checkReady(); - await this.updateSettingsFromServer(); // Fetch dynamic settings from the TTS server - await this.fetchTtsVoiceObjects(); // Fetch voices only if service is ready - this.updateNarratorVoicesDropdown(); - this.updateLanguageDropdown(); - this.setupEventListeners(); - this.applySettingsToHTML(); - updateStatus('Ready'); - } catch (error) { - console.error("Error loading settings:", error); - updateStatus('Offline'); - } - } - - applySettingsToHTML() { - // Apply loaded settings or use defaults - const narratorVoiceSelect = document.getElementById('narrator_voice'); - const atNarratorSelect = document.getElementById('at_narrator_enabled'); - const textNotInsideSelect = document.getElementById('at_narrator_text_not_inside'); - const generationMethodSelect = document.getElementById('at_generation_method'); - this.settings.narrator_voice = this.settings.narrator_voice_gen; - // Apply settings to Narrator Voice dropdown - if (narratorVoiceSelect && this.settings.narrator_voice) { - narratorVoiceSelect.value = this.settings.narrator_voice.replace('.wav', ''); - } - // Apply settings to AT Narrator Enabled dropdown - if (atNarratorSelect) { - // Sync the state with the checkbox in index.js - const ttsPassAsterisksCheckbox = document.getElementById('tts_pass_asterisks'); // Access the checkbox from index.js - const ttsNarrateQuotedCheckbox = document.getElementById('tts_narrate_quoted'); // Access the checkbox from index.js - const ttsNarrateDialoguesCheckbox = document.getElementById('tts_narrate_dialogues'); // Access the checkbox from index.js - // Sync the state with the checkbox in index.js - if (this.settings.narrator_enabled) { - ttsPassAsterisksCheckbox.checked = false; - $('#tts_pass_asterisks').click(); // Simulate a click event - $('#tts_pass_asterisks').trigger('change'); - } - if (!this.settings.narrator_enabled) { - ttsPassAsterisksCheckbox.checked = true; - $('#tts_pass_asterisks').click(); // Simulate a click event - $('#tts_pass_asterisks').trigger('change'); - } - // Uncheck and set tts_narrate_quoted to false if narrator is enabled - if (this.settings.narrator_enabledd) { - ttsNarrateQuotedCheckbox.checked = true; - ttsNarrateDialoguesCheckbox.checked = true; - // Trigger click events instead of change events - $('#tts_narrate_quoted').click(); - $('#tts_narrate_quoted').trigger('change'); - $('#tts_narrate_dialogues').click(); - $('#tts_narrate_dialogues').trigger('change'); - } - atNarratorSelect.value = this.settings.narrator_enabled.toString(); - this.settings.narrator_enabled = this.settings.narrator_enabled.toString(); - } - // Apply settings to the Language dropdown - const languageSelect = document.getElementById('language_options'); - if (languageSelect && this.settings.language) { - languageSelect.value = this.settings.language; - } - // Apply settings to Text Not Inside dropdown - if (textNotInsideSelect && this.settings.text_not_inside) { - textNotInsideSelect.value = this.settings.text_not_inside; - this.settings.at_narrator_text_not_inside = this.settings.text_not_inside; - } - // Apply settings to Generation Method dropdown - if (generationMethodSelect && this.settings.at_generation_method) { - generationMethodSelect.value = this.settings.at_generation_method; - } - // Additional logic to disable/enable dropdowns based on the selected generation method - const isStreamingEnabled = this.settings.at_generation_method === 'streaming_enabled'; - if (isStreamingEnabled) { - // Disable certain dropdowns when streaming is enabled - if (atNarratorSelect) atNarratorSelect.disabled = true; - if (textNotInsideSelect) textNotInsideSelect.disabled = true; - if (narratorVoiceSelect) narratorVoiceSelect.disabled = true; - } else { - // Enable dropdowns for standard generation - if (atNarratorSelect) atNarratorSelect.disabled = false; - if (textNotInsideSelect) textNotInsideSelect.disabled = !this.settings.narrator_enabled; - if (narratorVoiceSelect) narratorVoiceSelect.disabled = !this.settings.narrator_enabled; - } - const modelSelect = document.getElementById('switch_model'); - if (this.settings.finetuned_model === 'true') { - const ftOption = document.createElement('option'); - ftOption.value = 'XTTSv2 FT'; - ftOption.textContent = 'XTTSv2 FT'; - modelSelect.appendChild(ftOption); - } - } - - //##############################// - // Check AT Server is Available // - //##############################// - - async checkReady() { - try { - const response = await fetch(`${this.settings.provider_endpoint}/api/ready`); - // Check if the HTTP request was successful - if (!response.ok) { - throw new Error(`HTTP Error Response: ${response.status} ${response.statusText}`); - } - const statusText = await response.text(); - // Check if the response is 'Ready' - if (statusText === 'Ready') { - this.ready = true; // Set the ready flag to true - console.log('TTS service is ready.'); - } else { - this.ready = false; - console.log('TTS service is not ready.'); - } - } catch (error) { - console.error('Error checking TTS service readiness:', error); - this.ready = false; // Ensure ready flag is set to false in case of error - } - } - - //######################// - // Get Available Voices // - //######################// - - async fetchTtsVoiceObjects() { - const response = await fetch(`${this.settings.provider_endpoint}/api/voices`); - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`HTTP ${response.status}: ${errorText}`); - } - const data = await response.json(); - const voices = data.voices.map(filename => { - const voiceName = filename.replace('.wav', ''); - return { - name: voiceName, - voice_id: voiceName, - preview_url: null, // Preview URL will be dynamically generated - lang: 'en' // Default language - }; - }); - this.voices = voices; // Assign to the class property - return voices; // Also return this list - } - - //##########################################// - // Get Current AT Server Config & Update ST // - //##########################################// - - async updateSettingsFromServer() { - try { - // Fetch current settings - const response = await fetch(`${this.settings.provider_endpoint}/api/currentsettings`); - if (!response.ok) { - throw new Error(`Failed to fetch current settings: ${response.statusText}`); - } - const currentSettings = await response.json(); - // Update internal settings - this.settings.modelsAvailable = currentSettings.models_available; - this.settings.currentModel = currentSettings.current_model_loaded; - this.settings.deepspeed_available = currentSettings.deepspeed_available; - this.settings.deepSpeedEnabled = currentSettings.deepspeed_status; - this.settings.lowVramEnabled = currentSettings.low_vram_status; - this.settings.finetuned_model = currentSettings.finetuned_model - // Update HTML elements - this.updateModelDropdown(); - this.updateCheckboxes(); - } catch (error) { - console.error(`Error updating settings from server: ${error}`); - } - } - - //###################################################// - // Get Current AT Server Config & Update ST (Models) // - //###################################################// - - updateModelDropdown() { - const modelSelect = document.getElementById('switch_model'); - if (modelSelect) { - modelSelect.innerHTML = ''; // Clear existing options - this.settings.modelsAvailable.forEach(model => { - const option = document.createElement('option'); - option.value = model.model_name; - option.textContent = model.model_name; // Use model_name instead of name - option.selected = model.model_name === this.settings.currentModel; - modelSelect.appendChild(option); - }); - } - } - - //#######################################################// - // Get Current AT Server Config & Update ST (DS and LVR) // - //#######################################################// - - updateCheckboxes() { - const deepspeedCheckbox = document.getElementById('deepspeed'); - const lowVramCheckbox = document.getElementById('low_vram'); - if (lowVramCheckbox) lowVramCheckbox.checked = this.settings.lowVramEnabled; - if (deepspeedCheckbox) { - deepspeedCheckbox.checked = this.settings.deepSpeedEnabled; - deepspeedCheckbox.disabled = !this.settings.deepspeed_available; // Disable checkbox if deepspeed is not available - } - } - - //###############################################################// - // Get Current AT Server Config & Update ST (AT Narrator Voices) // - //###############################################################// - - updateNarratorVoicesDropdown() { - const narratorVoiceSelect = document.getElementById('narrator_voice'); - if (narratorVoiceSelect && this.voices) { - // Clear existing options - narratorVoiceSelect.innerHTML = ''; - // Add new options - for (let voice of this.voices) { - const option = document.createElement('option'); - option.value = voice.voice_id; - option.textContent = voice.name; - narratorVoiceSelect.appendChild(option); - } - } - } - - //######################################################// - // Get Current AT Server Config & Update ST (Languages) // - //######################################################// - - updateLanguageDropdown() { - const languageSelect = document.getElementById('language_options'); - if (languageSelect) { - // Ensure default language is set - this.settings.language = this.settings.language; - - languageSelect.innerHTML = ''; - for (let language in this.languageLabels) { - const option = document.createElement('option'); - option.value = this.languageLabels[language]; - option.textContent = language; - if (this.languageLabels[language] === this.settings.language) { - option.selected = true; - } - languageSelect.appendChild(option); - } - } - } - - //########################################// - // Start AT TTS extenstion page listeners // - //########################################// - - setupEventListeners() { - - let debounceTimeout; - const debounceDelay = 500; // Milliseconds - - // Define the event handler function - const onModelSelectChange = async (event) => { - console.log("Model select change event triggered"); // Debugging statement - const selectedModel = event.target.value; - console.log(`Selected model: ${selectedModel}`); // Debugging statement - // Set status to Processing - updateStatus('Processing'); - try { - const response = await fetch(`${this.settings.provider_endpoint}/api/reload?tts_method=${encodeURIComponent(selectedModel)}`, { - method: 'POST' - }); - if (!response.ok) { - throw new Error(`HTTP Error: ${response.status}`); - } - const data = await response.json(); - console.log("POST response data:", data); // Debugging statement - // Set status to Ready if successful - updateStatus('Ready'); - } catch (error) { - console.error("POST request error:", error); // Debugging statement - // Set status to Error in case of failure - updateStatus('Error'); - } - - // Handle response or error - }; - - const debouncedModelSelectChange = (event) => { - clearTimeout(debounceTimeout); - debounceTimeout = setTimeout(() => { - onModelSelectChange(event); - }, debounceDelay); - }; - - // Switch Model Listener - const modelSelect = document.getElementById('switch_model'); - if (modelSelect) { - // Remove the event listener if it was previously added - modelSelect.removeEventListener('change', debouncedModelSelectChange); - // Add the debounced event listener - modelSelect.addEventListener('change', debouncedModelSelectChange); - } - - // DeepSpeed Listener - const deepspeedCheckbox = document.getElementById('deepspeed'); - if (deepspeedCheckbox) { - deepspeedCheckbox.addEventListener('change', async (event) => { - const deepSpeedValue = event.target.checked ? 'True' : 'False'; - // Set status to Processing - updateStatus('Processing'); - try { - const response = await fetch(`${this.settings.provider_endpoint}/api/deepspeed?new_deepspeed_value=${deepSpeedValue}`, { - method: 'POST' - }); - if (!response.ok) { - throw new Error(`HTTP Error: ${response.status}`); - } - const data = await response.json(); - console.log("POST response data:", data); // Debugging statement - // Set status to Ready if successful - updateStatus('Ready'); - } catch (error) { - console.error("POST request error:", error); // Debugging statement - // Set status to Error in case of failure - updateStatus('Error'); - } - }); - } - - // Low VRAM Listener - const lowVramCheckbox = document.getElementById('low_vram'); - if (lowVramCheckbox) { - lowVramCheckbox.addEventListener('change', async (event) => { - const lowVramValue = event.target.checked ? 'True' : 'False'; - // Set status to Processing - updateStatus('Processing'); - try { - const response = await fetch(`${this.settings.provider_endpoint}/api/lowvramsetting?new_low_vram_value=${lowVramValue}`, { - method: 'POST' - }); - if (!response.ok) { - throw new Error(`HTTP Error: ${response.status}`); - } - const data = await response.json(); - console.log("POST response data:", data); // Debugging statement - // Set status to Ready if successful - updateStatus('Ready'); - } catch (error) { - console.error("POST request error:", error); // Debugging statement - // Set status to Error in case of failure - updateStatus('Error'); - } - }); - } - - // Narrator Voice Dropdown Listener - const narratorVoiceSelect = document.getElementById('narrator_voice'); - if (narratorVoiceSelect) { - narratorVoiceSelect.addEventListener('change', (event) => { - this.settings.narrator_voice_gen = `${event.target.value}.wav`; - this.onSettingsChange(); // Save the settings after change - }); - } - - const textNotInsideSelect = document.getElementById('at_narrator_text_not_inside'); - if (textNotInsideSelect) { - textNotInsideSelect.addEventListener('change', (event) => { - this.settings.text_not_inside = event.target.value; - this.onSettingsChange(); // Save the settings after change - }); - } - - // AT Narrator Dropdown Listener - const atNarratorSelect = document.getElementById('at_narrator_enabled'); - const ttsPassAsterisksCheckbox = document.getElementById('tts_pass_asterisks'); // Access the checkbox from index.js - const ttsNarrateQuotedCheckbox = document.getElementById('tts_narrate_quoted'); // Access the checkbox from index.js - const ttsNarrateDialoguesCheckbox = document.getElementById('tts_narrate_dialogues'); // Access the checkbox from index.js - - if (atNarratorSelect && textNotInsideSelect && narratorVoiceSelect) { - atNarratorSelect.addEventListener('change', (event) => { - const isNarratorEnabled = event.target.value === 'true'; - this.settings.narrator_enabled = isNarratorEnabled; // Update the setting here - textNotInsideSelect.disabled = !isNarratorEnabled; - narratorVoiceSelect.disabled = !isNarratorEnabled; - - // Sync the state with the checkbox in index.js - if (isNarratorEnabled) { - ttsPassAsterisksCheckbox.checked = false; - $('#tts_pass_asterisks').click(); // Simulate a click event - $('#tts_pass_asterisks').trigger('change'); - } - if (!isNarratorEnabled) { - ttsPassAsterisksCheckbox.checked = true; - $('#tts_pass_asterisks').click(); // Simulate a click event - $('#tts_pass_asterisks').trigger('change'); - } - // Uncheck and set tts_narrate_quoted to false if narrator is enabled - if (isNarratorEnabled) { - ttsNarrateQuotedCheckbox.checked = true; - ttsNarrateDialoguesCheckbox.checked = true; - // Trigger click events instead of change events - $('#tts_narrate_quoted').click(); - $('#tts_narrate_quoted').trigger('change'); - $('#tts_narrate_dialogues').click(); - $('#tts_narrate_dialogues').trigger('change'); - } - this.onSettingsChange(); // Save the settings after change - }); - } - - - // Event Listener for AT Generation Method Dropdown - const atGenerationMethodSelect = document.getElementById('at_generation_method'); - const atNarratorEnabledSelect = document.getElementById('at_narrator_enabled'); - if (atGenerationMethodSelect) { - atGenerationMethodSelect.addEventListener('change', (event) => { - const selectedMethod = event.target.value; - - if (selectedMethod === 'streaming_enabled') { - // Disable and unselect AT Narrator - atNarratorEnabledSelect.disabled = true; - atNarratorEnabledSelect.value = 'false'; - textNotInsideSelect.disabled = true; - narratorVoiceSelect.disabled = true; - } else if (selectedMethod === 'standard_generation') { - // Enable AT Narrator - atNarratorEnabledSelect.disabled = false; - } - this.settings.at_generation_method = selectedMethod; // Update the setting here - this.onSettingsChange(); // Save the settings after change - }); - } - - // Listener for Language Dropdown - const languageSelect = document.getElementById('language_options'); - if (languageSelect) { - languageSelect.addEventListener('change', (event) => { - this.settings.language = event.target.value; - this.onSettingsChange(); // Save the settings after change - }); - } - - // Listener for AllTalk Endpoint Input - const atServerInput = document.getElementById('at_server'); - if (atServerInput) { - atServerInput.addEventListener('input', (event) => { - this.settings.provider_endpoint = event.target.value; - this.onSettingsChange(); // Save the settings after change - }); - } - - } - - //#############################// - // Store ST interface settings // - //#############################// - - onSettingsChange() { - // Update settings based on the UI elements - //this.settings.provider_endpoint = $('#at_server').val(); - this.settings.language = $('#language_options').val(); - //this.settings.voiceMap = $('#voicemap').val(); - this.settings.at_generation_method = $('#at_generation_method').val(); - this.settings.narrator_enabled = $('#at_narrator_enabled').val(); - this.settings.at_narrator_text_not_inside = $('#at_narrator_text_not_inside').val(); - this.settings.narrator_voice_gen = $('#narrator_voice').val(); - // Save the updated settings - saveTtsProviderSettings(); - } - - //#########################// - // ST Handle Reload button // - //#########################// - - async onRefreshClick() { - await this.checkReady(); // Check if the TTS provider is ready - await this.loadSettings(this.settings); // Reload the settings - // Additional actions as needed - } - - //##################// - // Preview AT Voice // - //##################// - - async previewTtsVoice(voiceName) { - try { - // Prepare data for POST request - const postData = new URLSearchParams(); - postData.append("voice", `${voiceName}.wav`); - // Making the POST request - const response = await fetch(`${this.settings.provider_endpoint}/api/previewvoice/`, { - method: "POST", - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: postData, - }); - if (!response.ok) { - const errorText = await response.text(); - console.error(`[previewTtsVoice] Error Response Text:`, errorText); - throw new Error(`HTTP ${response.status}: ${errorText}`); - } - // Assuming the server returns a URL to the .wav file - const data = await response.json(); - if (data.output_file_url) { - // Use an audio element to play the .wav file - const audioElement = new Audio(data.output_file_url); - audioElement.play().catch(e => console.error("Error playing audio:", e)); - } else { - console.warn("[previewTtsVoice] No output file URL received in the response"); - throw new Error("No output file URL received in the response"); - } - - } catch (error) { - console.error("[previewTtsVoice] Exception caught during preview generation:", error); - throw error; - } - } - - //#####################// - // Populate ST voices // - //#####################// - - async getVoice(voiceName, generatePreview = false) { - // Ensure this.voices is populated - if (this.voices.length === 0) { - // Fetch voice objects logic - } - // Find the object where the name matches voiceName - const match = this.voices.find(voice => voice.name === voiceName); - if (!match) { - // Error handling - } - // Generate preview URL only if requested - if (!match.preview_url && generatePreview) { - // Generate preview logic - } - return match; // Return the found voice object - } - - //##########################################// - // Generate TTS Streaming or call Standard // - //##########################################// - - async generateTts(inputText, voiceId) { - try { - if (this.settings.at_generation_method === 'streaming_enabled') { - // Construct the streaming URL - const streamingUrl = `${this.settings.provider_endpoint}/api/tts-generate-streaming?text=${encodeURIComponent(inputText)}&voice=${encodeURIComponent(voiceId)}.wav&language=${encodeURIComponent(this.settings.language)}&output_file=stream_output.wav`; - console.log("Streaming URL:", streamingUrl); - - // Return the streaming URL directly - return streamingUrl; - } else { - // For standard method - const outputUrl = await this.fetchTtsGeneration(inputText, voiceId); - const audioResponse = await fetch(outputUrl); - if (!audioResponse.ok) { - throw new Error(`HTTP ${audioResponse.status}: Failed to fetch audio data`); - } - return audioResponse; // Return the fetch response directly - } - } catch (error) { - console.error("Error in generateTts:", error); - throw error; - } - } - - - //####################// - // Generate Standard // - //####################// - - async fetchTtsGeneration(inputText, voiceId) { - // Prepare the request payload - const requestBody = new URLSearchParams({ - 'text_input': inputText, - 'text_filtering': "standard", - 'character_voice_gen': voiceId + ".wav", - 'narrator_enabled': this.settings.narrator_enabled, - 'narrator_voice_gen': this.settings.narrator_voice_gen + ".wav", - 'text_not_inside': this.settings.at_narrator_text_not_inside, - 'language': this.settings.language, - 'output_file_name': "st_output", - 'output_file_timestamp': "true", - 'autoplay': "false", - 'autoplay_volume': "0.8" - }).toString(); - - try { - const response = await doExtrasFetch( - `${this.settings.provider_endpoint}/api/tts-generate`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Cache-Control': 'no-cache', - }, - body: requestBody - } - ); - - if (!response.ok) { - const errorText = await response.text(); - console.error(`[fetchTtsGeneration] Error Response Text:`, errorText); - // toastr.error(response.statusText, 'TTS Generation Failed'); - throw new Error(`HTTP ${response.status}: ${errorText}`); - } - const data = await response.json(); - const outputUrl = data.output_file_url; - return outputUrl; // Return only the output_file_url - } catch (error) { - console.error("[fetchTtsGeneration] Exception caught:", error); - throw error; // Rethrow the error for further handling - } - } -} - -//#########################// -// Update Status Messages // -//#########################// - -function updateStatus(message) { - const statusElement = document.getElementById('status_info'); - if (statusElement) { - statusElement.textContent = message; - switch (message) { - case 'Offline': - statusElement.style.color = 'red'; - break; - case 'Ready': - statusElement.style.color = 'lightgreen'; - break; - case 'Processing': - statusElement.style.color = 'blue'; - break; - case 'Error': - statusElement.style.color = 'red'; - break; - } - } -} \ No newline at end of file +import { doExtrasFetch } from '../../extensions.js'; +import { saveTtsProviderSettings } from './index.js'; + +export { AllTalkTtsProvider }; + +class AllTalkTtsProvider { + //########// + // Config // + //########// + + settings = {}; + constructor() { + // Initialize with default settings if they are not already set + this.settings = { + provider_endpoint: this.settings.provider_endpoint || 'http://localhost:7851', + language: this.settings.language || 'en', + voiceMap: this.settings.voiceMap || {}, + at_generation_method: this.settings.at_generation_method || 'standard_generation', + narrator_enabled: this.settings.narrator_enabled || 'false', + at_narrator_text_not_inside: this.settings.at_narrator_text_not_inside || 'narrator', + narrator_voice_gen: this.settings.narrator_voice_gen || 'female_01.wav', + finetuned_model: this.settings.finetuned_model || 'false', + }; + // Separate property for dynamically updated settings from the server + this.dynamicSettings = { + modelsAvailable: [], + currentModel: '', + deepspeed_available: false, + deepSpeedEnabled: false, + lowVramEnabled: false, + }; + } + ready = false; + voices = []; + separator = '. '; + audioElement = document.createElement('audio'); + + languageLabels = { + 'Arabic': 'ar', + 'Brazilian Portuguese': 'pt', + 'Chinese': 'zh-cn', + 'Czech': 'cs', + 'Dutch': 'nl', + 'English': 'en', + 'French': 'fr', + 'German': 'de', + 'Italian': 'it', + 'Polish': 'pl', + 'Russian': 'ru', + 'Spanish': 'es', + 'Turkish': 'tr', + 'Japanese': 'ja', + 'Korean': 'ko', + 'Hungarian': 'hu', + 'Hindi': 'hi', + }; + + get settingsHtml() { + let html = '
AllTalk Settings
'; + + html += `
+ +
+ + +
+
`; + + html += `
+ +
+ + +
+ +
+ + +
+ +
`; + + html += `
+
+ + +
+
+ + +
+
`; + + + html += `
+
+ + +
+ +
+ + +
+
`; + + + html += `
+
+ + +
+
+ + +
+
+ Status: Ready +
+
+ +
+
`; + + + html += `
+
+ AllTalk Config & Docs. +
+ +
+ AllTalk Website. +
+
`; + + html += `
+
+Text-generation-webui users - Uncheck Enable TTS in Text-generation-webui. +
+
`; + + return html; + } + + + //#################// + // Startup ST & AT // + //#################// + + async loadSettings(settings) { + updateStatus('Offline'); + + if (Object.keys(settings).length === 0) { + console.info('Using default AllTalk TTS Provider settings'); + } else { + // Populate settings with provided values, ignoring server-provided settings + for (const key in settings) { + if (key in this.settings) { + this.settings[key] = settings[key]; + } else { + console.debug(`Ignoring non-user-configurable setting: ${key}`); + } + } + } + + // Update UI elements to reflect the loaded settings + $('#at_server').val(this.settings.provider_endpoint); + $('#language_options').val(this.settings.language); + //$('#voicemap').val(this.settings.voiceMap); + $('#at_generation_method').val(this.settings.at_generation_method); + $('#at_narrator_enabled').val(this.settings.narrator_enabled); + $('#at_narrator_text_not_inside').val(this.settings.at_narrator_text_not_inside); + $('#narrator_voice').val(this.settings.narrator_voice_gen); + + console.debug('AllTalkTTS: Settings loaded'); + try { + // Check if TTS provider is ready + await this.checkReady(); + await this.updateSettingsFromServer(); // Fetch dynamic settings from the TTS server + await this.fetchTtsVoiceObjects(); // Fetch voices only if service is ready + this.updateNarratorVoicesDropdown(); + this.updateLanguageDropdown(); + this.setupEventListeners(); + this.applySettingsToHTML(); + updateStatus('Ready'); + } catch (error) { + console.error('Error loading settings:', error); + updateStatus('Offline'); + } + } + + applySettingsToHTML() { + // Apply loaded settings or use defaults + const narratorVoiceSelect = document.getElementById('narrator_voice'); + const atNarratorSelect = document.getElementById('at_narrator_enabled'); + const textNotInsideSelect = document.getElementById('at_narrator_text_not_inside'); + const generationMethodSelect = document.getElementById('at_generation_method'); + this.settings.narrator_voice = this.settings.narrator_voice_gen; + // Apply settings to Narrator Voice dropdown + if (narratorVoiceSelect && this.settings.narrator_voice) { + narratorVoiceSelect.value = this.settings.narrator_voice.replace('.wav', ''); + } + // Apply settings to AT Narrator Enabled dropdown + if (atNarratorSelect) { + // Sync the state with the checkbox in index.js + const ttsPassAsterisksCheckbox = document.getElementById('tts_pass_asterisks'); // Access the checkbox from index.js + const ttsNarrateQuotedCheckbox = document.getElementById('tts_narrate_quoted'); // Access the checkbox from index.js + const ttsNarrateDialoguesCheckbox = document.getElementById('tts_narrate_dialogues'); // Access the checkbox from index.js + // Sync the state with the checkbox in index.js + if (this.settings.narrator_enabled) { + ttsPassAsterisksCheckbox.checked = false; + $('#tts_pass_asterisks').click(); // Simulate a click event + $('#tts_pass_asterisks').trigger('change'); + } + if (!this.settings.narrator_enabled) { + ttsPassAsterisksCheckbox.checked = true; + $('#tts_pass_asterisks').click(); // Simulate a click event + $('#tts_pass_asterisks').trigger('change'); + } + // Uncheck and set tts_narrate_quoted to false if narrator is enabled + if (this.settings.narrator_enabledd) { + ttsNarrateQuotedCheckbox.checked = true; + ttsNarrateDialoguesCheckbox.checked = true; + // Trigger click events instead of change events + $('#tts_narrate_quoted').click(); + $('#tts_narrate_quoted').trigger('change'); + $('#tts_narrate_dialogues').click(); + $('#tts_narrate_dialogues').trigger('change'); + } + atNarratorSelect.value = this.settings.narrator_enabled.toString(); + this.settings.narrator_enabled = this.settings.narrator_enabled.toString(); + } + // Apply settings to the Language dropdown + const languageSelect = document.getElementById('language_options'); + if (languageSelect && this.settings.language) { + languageSelect.value = this.settings.language; + } + // Apply settings to Text Not Inside dropdown + if (textNotInsideSelect && this.settings.text_not_inside) { + textNotInsideSelect.value = this.settings.text_not_inside; + this.settings.at_narrator_text_not_inside = this.settings.text_not_inside; + } + // Apply settings to Generation Method dropdown + if (generationMethodSelect && this.settings.at_generation_method) { + generationMethodSelect.value = this.settings.at_generation_method; + } + // Additional logic to disable/enable dropdowns based on the selected generation method + const isStreamingEnabled = this.settings.at_generation_method === 'streaming_enabled'; + if (isStreamingEnabled) { + // Disable certain dropdowns when streaming is enabled + if (atNarratorSelect) atNarratorSelect.disabled = true; + if (textNotInsideSelect) textNotInsideSelect.disabled = true; + if (narratorVoiceSelect) narratorVoiceSelect.disabled = true; + } else { + // Enable dropdowns for standard generation + if (atNarratorSelect) atNarratorSelect.disabled = false; + if (textNotInsideSelect) textNotInsideSelect.disabled = !this.settings.narrator_enabled; + if (narratorVoiceSelect) narratorVoiceSelect.disabled = !this.settings.narrator_enabled; + } + const modelSelect = document.getElementById('switch_model'); + if (this.settings.finetuned_model === 'true') { + const ftOption = document.createElement('option'); + ftOption.value = 'XTTSv2 FT'; + ftOption.textContent = 'XTTSv2 FT'; + modelSelect.appendChild(ftOption); + } + } + + //##############################// + // Check AT Server is Available // + //##############################// + + async checkReady() { + try { + const response = await fetch(`${this.settings.provider_endpoint}/api/ready`); + // Check if the HTTP request was successful + if (!response.ok) { + throw new Error(`HTTP Error Response: ${response.status} ${response.statusText}`); + } + const statusText = await response.text(); + // Check if the response is 'Ready' + if (statusText === 'Ready') { + this.ready = true; // Set the ready flag to true + console.log('TTS service is ready.'); + } else { + this.ready = false; + console.log('TTS service is not ready.'); + } + } catch (error) { + console.error('Error checking TTS service readiness:', error); + this.ready = false; // Ensure ready flag is set to false in case of error + } + } + + //######################// + // Get Available Voices // + //######################// + + async fetchTtsVoiceObjects() { + const response = await fetch(`${this.settings.provider_endpoint}/api/voices`); + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`HTTP ${response.status}: ${errorText}`); + } + const data = await response.json(); + const voices = data.voices.map(filename => { + const voiceName = filename.replace('.wav', ''); + return { + name: voiceName, + voice_id: voiceName, + preview_url: null, // Preview URL will be dynamically generated + lang: 'en', // Default language + }; + }); + this.voices = voices; // Assign to the class property + return voices; // Also return this list + } + + //##########################################// + // Get Current AT Server Config & Update ST // + //##########################################// + + async updateSettingsFromServer() { + try { + // Fetch current settings + const response = await fetch(`${this.settings.provider_endpoint}/api/currentsettings`); + if (!response.ok) { + throw new Error(`Failed to fetch current settings: ${response.statusText}`); + } + const currentSettings = await response.json(); + // Update internal settings + this.settings.modelsAvailable = currentSettings.models_available; + this.settings.currentModel = currentSettings.current_model_loaded; + this.settings.deepspeed_available = currentSettings.deepspeed_available; + this.settings.deepSpeedEnabled = currentSettings.deepspeed_status; + this.settings.lowVramEnabled = currentSettings.low_vram_status; + this.settings.finetuned_model = currentSettings.finetuned_model; + // Update HTML elements + this.updateModelDropdown(); + this.updateCheckboxes(); + } catch (error) { + console.error(`Error updating settings from server: ${error}`); + } + } + + //###################################################// + // Get Current AT Server Config & Update ST (Models) // + //###################################################// + + updateModelDropdown() { + const modelSelect = document.getElementById('switch_model'); + if (modelSelect) { + modelSelect.innerHTML = ''; // Clear existing options + this.settings.modelsAvailable.forEach(model => { + const option = document.createElement('option'); + option.value = model.model_name; + option.textContent = model.model_name; // Use model_name instead of name + option.selected = model.model_name === this.settings.currentModel; + modelSelect.appendChild(option); + }); + } + } + + //#######################################################// + // Get Current AT Server Config & Update ST (DS and LVR) // + //#######################################################// + + updateCheckboxes() { + const deepspeedCheckbox = document.getElementById('deepspeed'); + const lowVramCheckbox = document.getElementById('low_vram'); + if (lowVramCheckbox) lowVramCheckbox.checked = this.settings.lowVramEnabled; + if (deepspeedCheckbox) { + deepspeedCheckbox.checked = this.settings.deepSpeedEnabled; + deepspeedCheckbox.disabled = !this.settings.deepspeed_available; // Disable checkbox if deepspeed is not available + } + } + + //###############################################################// + // Get Current AT Server Config & Update ST (AT Narrator Voices) // + //###############################################################// + + updateNarratorVoicesDropdown() { + const narratorVoiceSelect = document.getElementById('narrator_voice'); + if (narratorVoiceSelect && this.voices) { + // Clear existing options + narratorVoiceSelect.innerHTML = ''; + // Add new options + for (let voice of this.voices) { + const option = document.createElement('option'); + option.value = voice.voice_id; + option.textContent = voice.name; + narratorVoiceSelect.appendChild(option); + } + } + } + + //######################################################// + // Get Current AT Server Config & Update ST (Languages) // + //######################################################// + + updateLanguageDropdown() { + const languageSelect = document.getElementById('language_options'); + if (languageSelect) { + // Ensure default language is set + this.settings.language = this.settings.language; + + languageSelect.innerHTML = ''; + for (let language in this.languageLabels) { + const option = document.createElement('option'); + option.value = this.languageLabels[language]; + option.textContent = language; + if (this.languageLabels[language] === this.settings.language) { + option.selected = true; + } + languageSelect.appendChild(option); + } + } + } + + //########################################// + // Start AT TTS extenstion page listeners // + //########################################// + + setupEventListeners() { + + let debounceTimeout; + const debounceDelay = 500; // Milliseconds + + // Define the event handler function + const onModelSelectChange = async (event) => { + console.log('Model select change event triggered'); // Debugging statement + const selectedModel = event.target.value; + console.log(`Selected model: ${selectedModel}`); // Debugging statement + // Set status to Processing + updateStatus('Processing'); + try { + const response = await fetch(`${this.settings.provider_endpoint}/api/reload?tts_method=${encodeURIComponent(selectedModel)}`, { + method: 'POST', + }); + if (!response.ok) { + throw new Error(`HTTP Error: ${response.status}`); + } + const data = await response.json(); + console.log('POST response data:', data); // Debugging statement + // Set status to Ready if successful + updateStatus('Ready'); + } catch (error) { + console.error('POST request error:', error); // Debugging statement + // Set status to Error in case of failure + updateStatus('Error'); + } + + // Handle response or error + }; + + const debouncedModelSelectChange = (event) => { + clearTimeout(debounceTimeout); + debounceTimeout = setTimeout(() => { + onModelSelectChange(event); + }, debounceDelay); + }; + + // Switch Model Listener + const modelSelect = document.getElementById('switch_model'); + if (modelSelect) { + // Remove the event listener if it was previously added + modelSelect.removeEventListener('change', debouncedModelSelectChange); + // Add the debounced event listener + modelSelect.addEventListener('change', debouncedModelSelectChange); + } + + // DeepSpeed Listener + const deepspeedCheckbox = document.getElementById('deepspeed'); + if (deepspeedCheckbox) { + deepspeedCheckbox.addEventListener('change', async (event) => { + const deepSpeedValue = event.target.checked ? 'True' : 'False'; + // Set status to Processing + updateStatus('Processing'); + try { + const response = await fetch(`${this.settings.provider_endpoint}/api/deepspeed?new_deepspeed_value=${deepSpeedValue}`, { + method: 'POST', + }); + if (!response.ok) { + throw new Error(`HTTP Error: ${response.status}`); + } + const data = await response.json(); + console.log('POST response data:', data); // Debugging statement + // Set status to Ready if successful + updateStatus('Ready'); + } catch (error) { + console.error('POST request error:', error); // Debugging statement + // Set status to Error in case of failure + updateStatus('Error'); + } + }); + } + + // Low VRAM Listener + const lowVramCheckbox = document.getElementById('low_vram'); + if (lowVramCheckbox) { + lowVramCheckbox.addEventListener('change', async (event) => { + const lowVramValue = event.target.checked ? 'True' : 'False'; + // Set status to Processing + updateStatus('Processing'); + try { + const response = await fetch(`${this.settings.provider_endpoint}/api/lowvramsetting?new_low_vram_value=${lowVramValue}`, { + method: 'POST', + }); + if (!response.ok) { + throw new Error(`HTTP Error: ${response.status}`); + } + const data = await response.json(); + console.log('POST response data:', data); // Debugging statement + // Set status to Ready if successful + updateStatus('Ready'); + } catch (error) { + console.error('POST request error:', error); // Debugging statement + // Set status to Error in case of failure + updateStatus('Error'); + } + }); + } + + // Narrator Voice Dropdown Listener + const narratorVoiceSelect = document.getElementById('narrator_voice'); + if (narratorVoiceSelect) { + narratorVoiceSelect.addEventListener('change', (event) => { + this.settings.narrator_voice_gen = `${event.target.value}.wav`; + this.onSettingsChange(); // Save the settings after change + }); + } + + const textNotInsideSelect = document.getElementById('at_narrator_text_not_inside'); + if (textNotInsideSelect) { + textNotInsideSelect.addEventListener('change', (event) => { + this.settings.text_not_inside = event.target.value; + this.onSettingsChange(); // Save the settings after change + }); + } + + // AT Narrator Dropdown Listener + const atNarratorSelect = document.getElementById('at_narrator_enabled'); + const ttsPassAsterisksCheckbox = document.getElementById('tts_pass_asterisks'); // Access the checkbox from index.js + const ttsNarrateQuotedCheckbox = document.getElementById('tts_narrate_quoted'); // Access the checkbox from index.js + const ttsNarrateDialoguesCheckbox = document.getElementById('tts_narrate_dialogues'); // Access the checkbox from index.js + + if (atNarratorSelect && textNotInsideSelect && narratorVoiceSelect) { + atNarratorSelect.addEventListener('change', (event) => { + const isNarratorEnabled = event.target.value === 'true'; + this.settings.narrator_enabled = isNarratorEnabled; // Update the setting here + textNotInsideSelect.disabled = !isNarratorEnabled; + narratorVoiceSelect.disabled = !isNarratorEnabled; + + // Sync the state with the checkbox in index.js + if (isNarratorEnabled) { + ttsPassAsterisksCheckbox.checked = false; + $('#tts_pass_asterisks').click(); // Simulate a click event + $('#tts_pass_asterisks').trigger('change'); + } + if (!isNarratorEnabled) { + ttsPassAsterisksCheckbox.checked = true; + $('#tts_pass_asterisks').click(); // Simulate a click event + $('#tts_pass_asterisks').trigger('change'); + } + // Uncheck and set tts_narrate_quoted to false if narrator is enabled + if (isNarratorEnabled) { + ttsNarrateQuotedCheckbox.checked = true; + ttsNarrateDialoguesCheckbox.checked = true; + // Trigger click events instead of change events + $('#tts_narrate_quoted').click(); + $('#tts_narrate_quoted').trigger('change'); + $('#tts_narrate_dialogues').click(); + $('#tts_narrate_dialogues').trigger('change'); + } + this.onSettingsChange(); // Save the settings after change + }); + } + + + // Event Listener for AT Generation Method Dropdown + const atGenerationMethodSelect = document.getElementById('at_generation_method'); + const atNarratorEnabledSelect = document.getElementById('at_narrator_enabled'); + if (atGenerationMethodSelect) { + atGenerationMethodSelect.addEventListener('change', (event) => { + const selectedMethod = event.target.value; + + if (selectedMethod === 'streaming_enabled') { + // Disable and unselect AT Narrator + atNarratorEnabledSelect.disabled = true; + atNarratorEnabledSelect.value = 'false'; + textNotInsideSelect.disabled = true; + narratorVoiceSelect.disabled = true; + } else if (selectedMethod === 'standard_generation') { + // Enable AT Narrator + atNarratorEnabledSelect.disabled = false; + } + this.settings.at_generation_method = selectedMethod; // Update the setting here + this.onSettingsChange(); // Save the settings after change + }); + } + + // Listener for Language Dropdown + const languageSelect = document.getElementById('language_options'); + if (languageSelect) { + languageSelect.addEventListener('change', (event) => { + this.settings.language = event.target.value; + this.onSettingsChange(); // Save the settings after change + }); + } + + // Listener for AllTalk Endpoint Input + const atServerInput = document.getElementById('at_server'); + if (atServerInput) { + atServerInput.addEventListener('input', (event) => { + this.settings.provider_endpoint = event.target.value; + this.onSettingsChange(); // Save the settings after change + }); + } + + } + + //#############################// + // Store ST interface settings // + //#############################// + + onSettingsChange() { + // Update settings based on the UI elements + //this.settings.provider_endpoint = $('#at_server').val(); + this.settings.language = $('#language_options').val(); + //this.settings.voiceMap = $('#voicemap').val(); + this.settings.at_generation_method = $('#at_generation_method').val(); + this.settings.narrator_enabled = $('#at_narrator_enabled').val(); + this.settings.at_narrator_text_not_inside = $('#at_narrator_text_not_inside').val(); + this.settings.narrator_voice_gen = $('#narrator_voice').val(); + // Save the updated settings + saveTtsProviderSettings(); + } + + //#########################// + // ST Handle Reload button // + //#########################// + + async onRefreshClick() { + await this.checkReady(); // Check if the TTS provider is ready + await this.loadSettings(this.settings); // Reload the settings + // Additional actions as needed + } + + //##################// + // Preview AT Voice // + //##################// + + async previewTtsVoice(voiceName) { + try { + // Prepare data for POST request + const postData = new URLSearchParams(); + postData.append('voice', `${voiceName}.wav`); + // Making the POST request + const response = await fetch(`${this.settings.provider_endpoint}/api/previewvoice/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: postData, + }); + if (!response.ok) { + const errorText = await response.text(); + console.error('[previewTtsVoice] Error Response Text:', errorText); + throw new Error(`HTTP ${response.status}: ${errorText}`); + } + // Assuming the server returns a URL to the .wav file + const data = await response.json(); + if (data.output_file_url) { + // Use an audio element to play the .wav file + const audioElement = new Audio(data.output_file_url); + audioElement.play().catch(e => console.error('Error playing audio:', e)); + } else { + console.warn('[previewTtsVoice] No output file URL received in the response'); + throw new Error('No output file URL received in the response'); + } + + } catch (error) { + console.error('[previewTtsVoice] Exception caught during preview generation:', error); + throw error; + } + } + + //#####################// + // Populate ST voices // + //#####################// + + async getVoice(voiceName, generatePreview = false) { + // Ensure this.voices is populated + if (this.voices.length === 0) { + // Fetch voice objects logic + } + // Find the object where the name matches voiceName + const match = this.voices.find(voice => voice.name === voiceName); + if (!match) { + // Error handling + } + // Generate preview URL only if requested + if (!match.preview_url && generatePreview) { + // Generate preview logic + } + return match; // Return the found voice object + } + + //##########################################// + // Generate TTS Streaming or call Standard // + //##########################################// + + async generateTts(inputText, voiceId) { + try { + if (this.settings.at_generation_method === 'streaming_enabled') { + // Construct the streaming URL + const streamingUrl = `${this.settings.provider_endpoint}/api/tts-generate-streaming?text=${encodeURIComponent(inputText)}&voice=${encodeURIComponent(voiceId)}.wav&language=${encodeURIComponent(this.settings.language)}&output_file=stream_output.wav`; + console.log('Streaming URL:', streamingUrl); + + // Return the streaming URL directly + return streamingUrl; + } else { + // For standard method + const outputUrl = await this.fetchTtsGeneration(inputText, voiceId); + const audioResponse = await fetch(outputUrl); + if (!audioResponse.ok) { + throw new Error(`HTTP ${audioResponse.status}: Failed to fetch audio data`); + } + return audioResponse; // Return the fetch response directly + } + } catch (error) { + console.error('Error in generateTts:', error); + throw error; + } + } + + + //####################// + // Generate Standard // + //####################// + + async fetchTtsGeneration(inputText, voiceId) { + // Prepare the request payload + const requestBody = new URLSearchParams({ + 'text_input': inputText, + 'text_filtering': 'standard', + 'character_voice_gen': voiceId + '.wav', + 'narrator_enabled': this.settings.narrator_enabled, + 'narrator_voice_gen': this.settings.narrator_voice_gen + '.wav', + 'text_not_inside': this.settings.at_narrator_text_not_inside, + 'language': this.settings.language, + 'output_file_name': 'st_output', + 'output_file_timestamp': 'true', + 'autoplay': 'false', + 'autoplay_volume': '0.8', + }).toString(); + + try { + const response = await doExtrasFetch( + `${this.settings.provider_endpoint}/api/tts-generate`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Cache-Control': 'no-cache', + }, + body: requestBody, + }, + ); + + if (!response.ok) { + const errorText = await response.text(); + console.error('[fetchTtsGeneration] Error Response Text:', errorText); + // toastr.error(response.statusText, 'TTS Generation Failed'); + throw new Error(`HTTP ${response.status}: ${errorText}`); + } + const data = await response.json(); + const outputUrl = data.output_file_url; + return outputUrl; // Return only the output_file_url + } catch (error) { + console.error('[fetchTtsGeneration] Exception caught:', error); + throw error; // Rethrow the error for further handling + } + } +} + +//#########################// +// Update Status Messages // +//#########################// + +function updateStatus(message) { + const statusElement = document.getElementById('status_info'); + if (statusElement) { + statusElement.textContent = message; + switch (message) { + case 'Offline': + statusElement.style.color = 'red'; + break; + case 'Ready': + statusElement.style.color = 'lightgreen'; + break; + case 'Processing': + statusElement.style.color = 'blue'; + break; + case 'Error': + statusElement.style.color = 'red'; + break; + } + } +} diff --git a/public/scripts/extensions/tts/index.js b/public/scripts/extensions/tts/index.js index 8c72d7498..45a8d9d16 100644 --- a/public/scripts/extensions/tts/index.js +++ b/public/scripts/extensions/tts/index.js @@ -676,7 +676,7 @@ function onSkipCodeblocksClick() { function onPassAsterisksClick() { extension_settings.tts.pass_asterisks = !!$('#tts_pass_asterisks').prop('checked'); saveSettingsDebounced(); - console.log("setting pass asterisks", extension_settings.tts.pass_asterisks) + console.log('setting pass asterisks', extension_settings.tts.pass_asterisks); } //##############// diff --git a/public/scripts/textgen-settings.js b/public/scripts/textgen-settings.js index 51b08538d..df28104bf 100644 --- a/public/scripts/textgen-settings.js +++ b/public/scripts/textgen-settings.js @@ -547,7 +547,7 @@ jQuery(function () { inputElement.val(value).trigger('input'); if (power_user.enableZenSliders) { let masterElementID = inputElement.prop('id'); - console.log(masterElementID) + console.log(masterElementID); let zenSlider = $(`#${masterElementID}_zenslider`).slider(); zenSlider.slider('option', 'value', value); zenSlider.slider('option', 'slide') From b2509f8de475eb8b907e57c5fea2248a5aa419e4 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 20 Jan 2024 20:44:11 +0200 Subject: [PATCH 229/522] Rethrow AllTalk init error --- public/scripts/extensions/tts/alltalk.js | 1 + 1 file changed, 1 insertion(+) diff --git a/public/scripts/extensions/tts/alltalk.js b/public/scripts/extensions/tts/alltalk.js index fe48043f0..d51ac056c 100644 --- a/public/scripts/extensions/tts/alltalk.js +++ b/public/scripts/extensions/tts/alltalk.js @@ -312,6 +312,7 @@ class AllTalkTtsProvider { } catch (error) { console.error('Error checking TTS service readiness:', error); this.ready = false; // Ensure ready flag is set to false in case of error + throw error; // Rethrow the error for further handling } } From 4bc7fbcfd79fb76394e5ab39d680700be9af03c1 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 21 Jan 2024 15:07:35 +0200 Subject: [PATCH 230/522] Bump package version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 248e48e13..36cfb9528 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sillytavern", - "version": "1.11.2", + "version": "1.11.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sillytavern", - "version": "1.11.2", + "version": "1.11.3", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { diff --git a/package.json b/package.json index db996b17c..1d8eb4781 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "type": "git", "url": "https://github.com/SillyTavern/SillyTavern.git" }, - "version": "1.11.2", + "version": "1.11.3", "scripts": { "start": "node server.js", "start-multi": "node server.js --disableCsrf", From ffbf35e468627ef82764e785476282f6a5bb68e0 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 21 Jan 2024 15:11:47 +0200 Subject: [PATCH 231/522] Update index.js --- public/scripts/extensions/tts/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/extensions/tts/index.js b/public/scripts/extensions/tts/index.js index d9143998a..c018cc030 100644 --- a/public/scripts/extensions/tts/index.js +++ b/public/scripts/extensions/tts/index.js @@ -323,7 +323,7 @@ async function playAudioData(audioJob) { const srcUrl = await getBase64Async(audioBlob); // VRM lip sync - if (extension_settings.vrm.enabled && typeof window['vrmLipSync'] === 'function') { + if (extension_settings.vrm?.enabled && typeof window['vrmLipSync'] === 'function') { await window['vrmLipSync'](audioBlob, audioJob["char"]); } From e2becdf7a957284ebda60f0e4c1efc224cf153ca Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 21 Jan 2024 15:19:13 +0200 Subject: [PATCH 232/522] Add typedefs for TTS audioJob --- public/scripts/extensions/tts/index.js | 26 +++++++++++++++++++------- src/endpoints/assets.js | 2 +- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/public/scripts/extensions/tts/index.js b/public/scripts/extensions/tts/index.js index 6411bd2f7..992d973da 100644 --- a/public/scripts/extensions/tts/index.js +++ b/public/scripts/extensions/tts/index.js @@ -298,7 +298,7 @@ function debugTtsPlayback() { }, )); } -window.debugTtsPlayback = debugTtsPlayback; +window['debugTtsPlayback'] = debugTtsPlayback; //##################// // Audio Control // @@ -308,13 +308,25 @@ let audioElement = new Audio(); audioElement.id = 'tts_audio'; audioElement.autoplay = true; +/** + * @type AudioJob[] Audio job queue + * @typedef {{audioBlob: Blob | string, char: string}} AudioJob Audio job object + */ let audioJobQueue = []; +/** + * @type AudioJob Current audio job + */ let currentAudioJob; let audioPaused = false; let audioQueueProcessorReady = true; +/** + * Play audio data from audio job object. + * @param {AudioJob} audioJob Audio job object + * @returns {Promise} Promise that resolves when audio playback is started + */ async function playAudioData(audioJob) { - const audioBlob = audioJob["audioBlob"]; + const { audioBlob, char } = audioJob; // Since current audio job can be cancelled, don't playback if it is null if (currentAudioJob == null) { console.log('Cancelled TTS playback because currentAudioJob was null'); @@ -324,7 +336,7 @@ async function playAudioData(audioJob) { // VRM lip sync if (extension_settings.vrm?.enabled && typeof window['vrmLipSync'] === 'function') { - await window['vrmLipSync'](audioBlob, audioJob["char"]); + await window['vrmLipSync'](audioBlob, char); } audioElement.src = srcUrl; @@ -343,7 +355,7 @@ async function playAudioData(audioJob) { window['tts_preview'] = function (id) { const audio = document.getElementById(id); - if (audio && !$(audio).data('disabled')) { + if (audio instanceof HTMLAudioElement && !$(audio).data('disabled')) { audio.play(); } else { @@ -429,13 +441,13 @@ function completeCurrentAudioJob() { */ async function addAudioJob(response, char) { if (typeof response === 'string') { - audioJobQueue.push({"audioBlob":response, "char":char}); + audioJobQueue.push({ audioBlob: response, char: char }); } else { const audioData = await response.blob(); if (!audioData.type.startsWith('audio/')) { throw `TTS received HTTP response with invalid data format. Expecting audio/*, got ${audioData.type}`; } - audioJobQueue.push({"audioBlob":audioData, "char":char}); + audioJobQueue.push({ audioBlob: audioData, char: char }); } console.debug('Pushed audio job to queue.'); } @@ -576,7 +588,7 @@ async function playFullConversation() { const chat = context.chat; ttsJobQueue = chat; } -window.playFullConversation = playFullConversation; +window['playFullConversation'] = playFullConversation; //#############################// // Extension UI and Settings // diff --git a/src/endpoints/assets.js b/src/endpoints/assets.js index 0bd160cc2..0284c9e82 100644 --- a/src/endpoints/assets.js +++ b/src/endpoints/assets.js @@ -108,7 +108,7 @@ router.post('/get', jsonParser, async (_, response) => { // VRM assets if (folder == 'vrm') { - output[folder] = {'model':[], 'animation':[]}; + output[folder] = { 'model': [], 'animation': [] }; // Extract models const vrm_model_folder = path.normalize(path.join(folderPath, 'vrm', 'model')); let files = getFiles(vrm_model_folder); From 814ed49c3138eda604d5773f0f20166e65f8e738 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 21 Jan 2024 17:27:09 +0200 Subject: [PATCH 233/522] #1719 Clear text nodes in rendered formulas --- public/scripts/RossAscends-mods.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/public/scripts/RossAscends-mods.js b/public/scripts/RossAscends-mods.js index 7b76ba010..a75e58ec5 100644 --- a/public/scripts/RossAscends-mods.js +++ b/public/scripts/RossAscends-mods.js @@ -60,6 +60,16 @@ const observer = new MutationObserver(function (mutations) { RA_checkOnlineStatus(); } else if (mutation.target.parentNode === SelectedCharacterTab) { setTimeout(RA_CountCharTokens, 200); + } else if (mutation.target.classList.contains('mes_text')) { + if (mutation.target instanceof HTMLElement) { + for (const element of mutation.target.getElementsByTagName('math')) { + element.childNodes.forEach(function (child) { + if (child.nodeType === Node.TEXT_NODE) { + child.textContent = ''; + } + }); + } + } } }); }); @@ -1006,7 +1016,7 @@ export function initRossMods() { Don't ask again `; - callPopup(popupText, 'confirm').then(result =>{ + callPopup(popupText, 'confirm').then(result => { if (!result) { return; } From 3cd935c0d26718afe84cd7bbc048d6fa3a7736f2 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 21 Jan 2024 23:13:01 +0200 Subject: [PATCH 234/522] Fix possible prompt overflow on message examples push-out --- public/script.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/script.js b/public/script.js index 3e491540a..d8d7f8537 100644 --- a/public/script.js +++ b/public/script.js @@ -3314,7 +3314,8 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu const prompt = [ storyString, mesExmString, - mesSend.join(''), + mesSend.map((e) => `${e.extensionPrompts.join('')}${e.message}`).join(''), + '\n', generatedPromptCache, allAnchors, quiet_prompt, From 958cf6a373d612485ccfaaec681797521f750cfe Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 21 Jan 2024 23:20:29 +0200 Subject: [PATCH 235/522] Don't append name2 in non-instruct mode if continuing on first message --- public/script.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/public/script.js b/public/script.js index d8d7f8537..37d3a4633 100644 --- a/public/script.js +++ b/public/script.js @@ -3275,8 +3275,9 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu } // Add character's name - // Force name append on continue (if not continuing on user message) - if (!isInstruct && force_name2) { + // Force name append on continue (if not continuing on user message or first message) + const isContinuingOnFirstMessage = chat.length === 1 && isContinue; + if (!isInstruct && force_name2 && !isContinuingOnFirstMessage) { if (!lastMesString.endsWith('\n')) { lastMesString += '\n'; } From 6a03980db699f2713ca1bdcbff18e7b52f02817f Mon Sep 17 00:00:00 2001 From: Juha Jeronen Date: Mon, 22 Jan 2024 15:56:12 +0200 Subject: [PATCH 236/522] add/improve tooltips --- public/index.html | 168 ++++++++++-------- public/scripts/extensions/memory/index.js | 8 +- .../scripts/extensions/vectors/settings.html | 2 +- 3 files changed, 103 insertions(+), 75 deletions(-) diff --git a/public/index.html b/public/index.html index abdf53a5b..16380a573 100644 --- a/public/index.html +++ b/public/index.html @@ -259,7 +259,7 @@
@@ -432,7 +432,7 @@

-

@@ -1341,7 +1356,7 @@

Mirostat -
+

@@ -1350,12 +1365,18 @@
- Tau + + Tau +
+
- Eta + + Eta +
+
@@ -1385,11 +1406,14 @@
-

Contrast Search -
+

Contrastive Search +

- Penalty Alpha + + Penalty Alpha +
+
@@ -1427,7 +1451,7 @@ @@ -1469,7 +1493,7 @@

CFG -
+

Scale @@ -3189,59 +3213,59 @@

Theme Toggles

-
-
- MUI Preset: -
- - +
-