diff --git a/public/css/rm-groups.css b/public/css/rm-groups.css index 4bc82ee6b..e7cbb2c95 100644 --- a/public/css/rm-groups.css +++ b/public/css/rm-groups.css @@ -58,6 +58,11 @@ cursor: unset; } +#rm_group_buttons textarea { + margin: 0px; + min-width: 200px; +} + #rm_group_members, #rm_group_add_members { margin-top: 0.25rem; diff --git a/public/index.html b/public/index.html index 944235149..433414926 100644 --- a/public/index.html +++ b/public/index.html @@ -4375,24 +4375,44 @@
-
+
+
-
+
+
+
+ + +
+
+ + +
@@ -4506,6 +4526,22 @@
+
diff --git a/public/script.js b/public/script.js index dd697b331..2c7390614 100644 --- a/public/script.js +++ b/public/script.js @@ -212,6 +212,7 @@ import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, de import { initPresetManager } from './scripts/preset-manager.js'; import { evaluateMacros } from './scripts/macros.js'; import { currentUser, setUserControls } from './scripts/user.js'; +import { callGenericPopup } from './scripts/popup.js'; //exporting functions and vars for mods export { @@ -822,7 +823,7 @@ let create_save = { //animation right menu export const ANIMATION_DURATION_DEFAULT = 125; export let animation_duration = ANIMATION_DURATION_DEFAULT; -let animation_easing = 'ease-in-out'; +export let animation_easing = 'ease-in-out'; let popup_type = ''; let chat_file_for_del = ''; let online_status = 'no_connection'; @@ -3465,7 +3466,6 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu // 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); @@ -3512,10 +3512,11 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu function getMessagesTokenCount() { const encodeString = [ + beforeScenarioAnchor, storyString, + afterScenarioAnchor, examplesString, chatString, - allAnchors, quiet_prompt, cyclePrompt, userAlignmentMessage, @@ -3783,12 +3784,13 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu console.debug('---checking Prompt size'); setPromptString(); const prompt = [ + beforeScenarioAnchor, storyString, + afterScenarioAnchor, mesExmString, mesSend.map((e) => `${e.extensionPrompts.join('')}${e.message}`).join(''), '\n', generatedPromptCache, - allAnchors, quiet_prompt, ].join('').replace(/\r/gm, ''); let thisPromptContextSize = getTokenCount(prompt, power_user.token_padding); @@ -4024,7 +4026,8 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu ...thisPromptBits[currentArrayEntry], rawPrompt: generate_data.prompt || generate_data.input, mesId: getNextMessageId(type), - allAnchors: allAnchors, + allAnchors: getAllExtensionPrompts(), + chatInjects: injectedIndices?.map(index => arrMes[arrMes.length - index - 1])?.join('') || '', summarizeString: (extension_prompts['1_memory']?.value || ''), authorsNoteString: (extension_prompts['2_floating_prompt']?.value || ''), smartContextString: (extension_prompts['chromadb']?.value || ''), @@ -4648,8 +4651,13 @@ function promptItemize(itemizedPrompts, requestedMesId) { zeroDepthAnchorTokens: getTokenCount(itemizedPrompts[thisPromptSet].zeroDepthAnchor), // TODO: unused thisPrompt_padding: itemizedPrompts[thisPromptSet].padding, this_main_api: itemizedPrompts[thisPromptSet].main_api, + chatInjects: getTokenCount(itemizedPrompts[thisPromptSet].chatInjects), }; + if (params.chatInjects){ + params.ActualChatHistoryTokens = params.ActualChatHistoryTokens - params.chatInjects; + } + if (params.this_main_api == 'openai') { //for OAI API //console.log('-- Counting OAI Tokens'); @@ -7809,6 +7817,7 @@ window['SillyTavern'].getContext = function () { registedDebugFunction: registerDebugFunction, renderExtensionTemplate: renderExtensionTemplate, callPopup: callPopup, + callGenericPopup: callGenericPopup, mainApi: main_api, extensionSettings: extension_settings, ModuleWorkerWrapper: ModuleWorkerWrapper, diff --git a/public/scripts/extensions/assets/index.js b/public/scripts/extensions/assets/index.js index c71ffb620..66f5e55cb 100644 --- a/public/scripts/extensions/assets/index.js +++ b/public/scripts/extensions/assets/index.js @@ -103,7 +103,8 @@ function downloadAssetsList(url) { if (assetType == 'extension') { assetTypeMenu.append(`
- To download extensions from this page, you need to have Git installed. + To download extensions from this page, you need to have Git installed.
+ Click the icon to visit the Extension's repo for tips on how to use it.
`); } @@ -180,6 +181,7 @@ function downloadAssetsList(url) { const displayName = DOMPurify.sanitize(asset['name'] || asset['id']); const description = DOMPurify.sanitize(asset['description'] || ''); const url = isValidUrl(asset['url']) ? asset['url'] : ''; + const title = assetType === 'extension' ? `Extension repo/guide: ${url}` : 'Preview in browser'; const previewIcon = (assetType === 'extension' || assetType === 'character') ? 'fa-arrow-up-right-from-square' : 'fa-headphones-simple'; const assetBlock = $('') @@ -187,7 +189,7 @@ function downloadAssetsList(url) { .append(`
${displayName} - + diff --git a/public/scripts/extensions/expressions/index.js b/public/scripts/extensions/expressions/index.js index ac0493e37..4ee04f552 100644 --- a/public/scripts/extensions/expressions/index.js +++ b/public/scripts/extensions/expressions/index.js @@ -11,7 +11,7 @@ const MODULE_NAME = 'expressions'; const UPDATE_INTERVAL = 2000; const STREAMING_UPDATE_INTERVAL = 6000; const TALKINGCHECK_UPDATE_INTERVAL = 500; -const FALLBACK_EXPRESSION = 'joy'; +const DEFAULT_FALLBACK_EXPRESSION = 'joy'; const DEFAULT_EXPRESSIONS = [ 'talkinghead', 'admiration', @@ -58,6 +58,14 @@ function isTalkingHeadEnabled() { return extension_settings.expressions.talkinghead && !extension_settings.expressions.local; } +/** + * Returns the fallback expression if explicitly chosen, otherwise the default one + * @returns {string} expression name + */ +function getFallbackExpression() { + return extension_settings.expressions.fallback_expression ?? DEFAULT_FALLBACK_EXPRESSION; +} + /** * Toggles Talkinghead mode on/off. * @@ -157,7 +165,8 @@ async function visualNovelSetCharacterSprites(container, name, expression) { const sprites = spriteCache[spriteFolderName]; const expressionImage = container.find(`.expression-holder[data-avatar="${avatar}"]`); - const defaultSpritePath = sprites.find(x => x.label === FALLBACK_EXPRESSION)?.path; + const defaultExpression = getFallbackExpression(); + const defaultSpritePath = sprites.find(x => x.label === defaultExpression)?.path; const noSprites = sprites.length === 0; if (expressionImage.length > 0) { @@ -568,7 +577,7 @@ function handleImageChange() { // This preserves the same expression Talkinghead had at the moment it was switched off. const charName = getContext().name2; const last = lastExpression[charName]; - const targetExpression = last ? last : FALLBACK_EXPRESSION; + const targetExpression = last ? last : getFallbackExpression(); setExpression(charName, targetExpression, true); } } @@ -691,8 +700,8 @@ async function moduleWorker() { const force = !!context.groupId; // Character won't be angry on you for swiping - if (currentLastMessage.mes == '...' && expressionsList.includes(FALLBACK_EXPRESSION)) { - expression = FALLBACK_EXPRESSION; + if (currentLastMessage.mes == '...' && expressionsList.includes(getFallbackExpression())) { + expression = getFallbackExpression(); } await sendExpressionCall(spriteFolderName, expression, force, vnMode); @@ -965,7 +974,7 @@ function sampleClassifyText(text) { async function getExpressionLabel(text) { // Return if text is undefined, saving a costly fetch request if ((!modules.includes('classify') && !extension_settings.expressions.local) || !text) { - return FALLBACK_EXPRESSION; + return getFallbackExpression(); } text = sampleClassifyText(text); @@ -1004,7 +1013,7 @@ async function getExpressionLabel(text) { } } catch (error) { console.log(error); - return FALLBACK_EXPRESSION; + return getFallbackExpression(); } } @@ -1108,6 +1117,11 @@ async function getSpritesList(name) { } } +async function renderAdditionalExpressionSettings() { + renderCustomExpressions(); + await renderFallbackExpressionPicker(); +} + function renderCustomExpressions() { if (!Array.isArray(extension_settings.expressions.custom)) { extension_settings.expressions.custom = []; @@ -1128,6 +1142,23 @@ function renderCustomExpressions() { } } +async function renderFallbackExpressionPicker() { + const expressions = await getExpressionsList(); + + const defaultPicker = $('#expression_fallback'); + defaultPicker.empty(); + + const fallbackExpression = getFallbackExpression(); + + for (const expression of expressions) { + const option = document.createElement('option'); + option.value = expression; + option.text = expression; + option.selected = expression == fallbackExpression; + defaultPicker.append(option); + } +} + async function getExpressionsList() { // Return cached list if available if (Array.isArray(expressionsList)) { @@ -1365,7 +1396,7 @@ async function onClickExpressionAddCustom() { // Add custom expression into settings extension_settings.expressions.custom.push(expressionName); - renderCustomExpressions(); + await renderAdditionalExpressionSettings(); saveSettingsDebounced(); // Force refresh sprites list @@ -1392,7 +1423,11 @@ async function onClickExpressionRemoveCustom() { // Remove custom expression from settings const index = extension_settings.expressions.custom.indexOf(selectedExpression); extension_settings.expressions.custom.splice(index, 1); - renderCustomExpressions(); + if (selectedExpression == getFallbackExpression()) { + toastr.warning(`Deleted custom expression '${selectedExpression}' that was also selected as the fallback expression.\nFallback expression has been reset to '${DEFAULT_FALLBACK_EXPRESSION}'.`); + extension_settings.expressions.fallback_expression = DEFAULT_FALLBACK_EXPRESSION; + } + await renderAdditionalExpressionSettings(); saveSettingsDebounced(); // Force refresh sprites list @@ -1401,6 +1436,14 @@ async function onClickExpressionRemoveCustom() { moduleWorker(); } +function onExpressionFallbackChanged() { + const expression = this.value; + if (expression) { + extension_settings.expressions.fallback_expression = expression; + saveSettingsDebounced(); + } +} + async function handleFileUpload(url, formData) { try { const data = await jQuery.ajax({ @@ -1648,7 +1691,7 @@ async function fetchImagesNoCache() { return await Promise.allSettled(promises); } -(function () { +(async function () { function addExpressionImage() { const html = `
@@ -1668,7 +1711,7 @@ async function fetchImagesNoCache() { element.hide(); $('body').append(element); } - function addSettings() { + async function addSettings() { $('#extensions_settings').append(renderExtensionTemplate(MODULE_NAME, 'settings')); $('#expression_override_button').on('click', onClickExpressionOverrideButton); $('#expressions_show_default').on('input', onExpressionsShowDefaultInput); @@ -1696,10 +1739,11 @@ async function fetchImagesNoCache() { } }); - renderCustomExpressions(); + await renderAdditionalExpressionSettings(); $('#expression_custom_add').on('click', onClickExpressionAddCustom); $('#expression_custom_remove').on('click', onClickExpressionRemoveCustom); + $('#expression_fallback').on('change', onExpressionFallbackChanged) } // Pause Talkinghead to save resources when the ST tab is not visible or the window is minimized. @@ -1732,7 +1776,7 @@ async function fetchImagesNoCache() { addExpressionImage(); addVisualNovelMode(); - addSettings(); + await addSettings(); const wrapper = new ModuleWorkerWrapper(moduleWorker); const updateFunction = wrapper.update.bind(wrapper); setInterval(updateFunction, UPDATE_INTERVAL); diff --git a/public/scripts/extensions/expressions/settings.html b/public/scripts/extensions/expressions/settings.html index 89e73fa63..4abc49fcc 100644 --- a/public/scripts/extensions/expressions/settings.html +++ b/public/scripts/extensions/expressions/settings.html @@ -18,6 +18,11 @@ Image Type - talkinghead (extras) +
+ + Set the default and fallback expression being used when no matching expression is found. + +
Can be set manually or with an /emote slash command. diff --git a/public/scripts/extensions/quick-reply/html/qrEditor.html b/public/scripts/extensions/quick-reply/html/qrEditor.html index 08cecbc23..8ed195520 100644 --- a/public/scripts/extensions/quick-reply/html/qrEditor.html +++ b/public/scripts/extensions/quick-reply/html/qrEditor.html @@ -16,12 +16,20 @@ - +
- + + +
diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index 2cc817fb5..33a1d05f6 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -1,4 +1,4 @@ -import { callPopup } from '../../../../script.js'; +import { POPUP_TYPE, Popup } from '../../../popup.js'; import { getSortableDelay } from '../../../utils.js'; import { log, warn } from '../index.js'; import { QuickReplyContextLink } from './QuickReplyContextLink.js'; @@ -44,6 +44,13 @@ export class QuickReply { /**@type {HTMLInputElement}*/ settingsDomLabel; /**@type {HTMLTextAreaElement}*/ settingsDomMessage; + /**@type {Popup}*/ editorPopup; + + /**@type {HTMLElement}*/ editorExecuteBtn; + /**@type {HTMLElement}*/ editorExecuteErrors; + /**@type {HTMLInputElement}*/ editorExecuteHide; + /**@type {Promise}*/ editorExecutePromise; + get hasContext() { return this.contextList && this.contextList.length > 0; @@ -192,7 +199,8 @@ export class QuickReply { /**@type {HTMLElement} */ // @ts-ignore const dom = this.template.cloneNode(true); - const popupResult = callPopup(dom, 'text', undefined, { okButton: 'OK', wide: true, large: true, rows: 1 }); + this.editorPopup = new Popup(dom, POPUP_TYPE.TEXT, undefined, { okButton: 'OK', wide: true, large: true, rows: 1 }); + const popupResult = this.editorPopup.show(); // basics /**@type {HTMLInputElement}*/ @@ -209,7 +217,7 @@ export class QuickReply { }); /**@type {HTMLInputElement}*/ const wrap = dom.querySelector('#qr--modal-wrap'); - wrap.checked = JSON.parse(localStorage.getItem('qr--wrap')); + wrap.checked = JSON.parse(localStorage.getItem('qr--wrap') ?? 'false'); wrap.addEventListener('click', () => { localStorage.setItem('qr--wrap', JSON.stringify(wrap.checked)); updateWrap(); @@ -221,9 +229,26 @@ export class QuickReply { message.style.whiteSpace = 'pre'; } }; + /**@type {HTMLInputElement}*/ + const tabSize = dom.querySelector('#qr--modal-tabSize'); + tabSize.value = JSON.parse(localStorage.getItem('qr--tabSize') ?? '4'); + const updateTabSize = () => { + message.style.tabSize = tabSize.value; + }; + tabSize.addEventListener('change', () => { + localStorage.setItem('qr--tabSize', JSON.stringify(Number(tabSize.value))); + updateTabSize(); + }); + /**@type {HTMLInputElement}*/ + const executeShortcut = dom.querySelector('#qr--modal-executeShortcut'); + executeShortcut.checked = JSON.parse(localStorage.getItem('qr--executeShortcut') ?? 'true'); + executeShortcut.addEventListener('click', () => { + localStorage.setItem('qr--executeShortcut', JSON.stringify(executeShortcut.checked)); + }); /**@type {HTMLTextAreaElement}*/ const message = dom.querySelector('#qr--modal-message'); updateWrap(); + updateTabSize(); message.value = this.message; message.addEventListener('input', () => { this.updateMessage(message.value); @@ -257,6 +282,12 @@ export class QuickReply { message.selectionStart = start - 1; message.selectionEnd = end - count; this.updateMessage(message.value); + } else if (evt.key == 'Enter' && evt.ctrlKey && !evt.shiftKey && !evt.altKey) { + evt.stopPropagation(); + evt.preventDefault(); + if (executeShortcut.checked) { + this.executeFromEditor(); + } } }); @@ -385,27 +416,15 @@ export class QuickReply { /**@type {HTMLElement}*/ const executeErrors = dom.querySelector('#qr--modal-executeErrors'); + this.editorExecuteErrors = executeErrors; /**@type {HTMLInputElement}*/ const executeHide = dom.querySelector('#qr--modal-executeHide'); - let executePromise; + this.editorExecuteHide = executeHide; /**@type {HTMLElement}*/ const executeBtn = dom.querySelector('#qr--modal-execute'); + this.editorExecuteBtn = executeBtn; 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.execute(); - await executePromise; - } catch (ex) { - executeErrors.textContent = ex.message; - } - executePromise = null; - executeBtn.classList.remove('qr--busy'); - document.querySelector('#shadow_popup').classList.remove('qr--hide'); + await this.executeFromEditor(); }); await popupResult; @@ -414,6 +433,24 @@ export class QuickReply { } } + async executeFromEditor() { + if (this.editorExecutePromise) return; + this.editorExecuteBtn.classList.add('qr--busy'); + this.editorExecuteErrors.innerHTML = ''; + if (this.editorExecuteHide.checked) { + this.editorPopup.dom.classList.add('qr--hide'); + } + try { + this.editorExecutePromise = this.execute(); + await this.editorExecutePromise; + } catch (ex) { + this.editorExecuteErrors.textContent = ex.message; + } + this.editorExecutePromise = null; + this.editorExecuteBtn.classList.remove('qr--busy'); + this.editorPopup.dom.classList.remove('qr--hide'); + } + diff --git a/public/scripts/extensions/quick-reply/style.css b/public/scripts/extensions/quick-reply/style.css index 74bf8134f..bff6ee067 100644 --- a/public/scripts/extensions/quick-reply/style.css +++ b/public/scripts/extensions/quick-reply/style.css @@ -216,71 +216,85 @@ align-items: baseline; } @media screen and (max-width: 750px) { - body #dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor { + body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor { flex-direction: column; } - body #dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels { + body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels { flex-direction: column; } - body #dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-message { + body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-message { min-height: 90svh; } } -#dialogue_popup:has(#qr--modalEditor) { +.dialogue_popup:has(#qr--modalEditor) { aspect-ratio: unset; } -#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text { +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text { display: flex; flex-direction: column; } -#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor { +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor { flex: 1 1 auto; display: flex; flex-direction: row; gap: 1em; } -#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main { +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main { flex: 1 1 auto; display: flex; flex-direction: column; } -#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels { +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels { flex: 0 0 auto; display: flex; flex-direction: row; gap: 0.5em; } -#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label { +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label { flex: 1 1 1px; display: flex; flex-direction: column; } -#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label > .qr--labelText { +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label > .qr--labelText { flex: 1 1 auto; } -#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label > .qr--labelHint { +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label > .qr--labelHint { flex: 1 1 auto; } -#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label > input { +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels > label > input { flex: 0 0 auto; } -#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer { +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer { flex: 1 1 auto; display: flex; flex-direction: column; } -#dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-message { +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings { + display: flex; + flex-direction: row; + gap: 1em; + color: var(--grey70); + font-size: smaller; + align-items: baseline; +} +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings > .checkbox_label { + white-space: nowrap; +} +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings > .checkbox_label > input { + font-size: inherit; +} +.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 { +.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 { +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-execute.qr--busy { opacity: 0.5; cursor: wait; } -#shadow_popup.qr--hide { +.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 e6271a2c0..b91d7b444 100644 --- a/public/scripts/extensions/quick-reply/style.less +++ b/public/scripts/extensions/quick-reply/style.less @@ -242,7 +242,7 @@ @media screen and (max-width: 750px) { - body #dialogue_popup:has(#qr--modalEditor) #dialogue_popup_text > #qr--modalEditor { + body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor { flex-direction: column; > #qr--main > .qr--labels { flex-direction: column; @@ -252,10 +252,10 @@ } } } -#dialogue_popup:has(#qr--modalEditor) { +.dialogue_popup:has(#qr--modalEditor) { aspect-ratio: unset; - #dialogue_popup_text { + .dialogue_popup_text { display: flex; flex-direction: column; @@ -293,6 +293,20 @@ flex: 1 1 auto; display: flex; flex-direction: column; + > .qr--modal-editorSettings { + display: flex; + flex-direction: row; + gap: 1em; + color: var(--grey70); + font-size: smaller; + align-items: baseline; + > .checkbox_label { + white-space: nowrap; + > input { + font-size: inherit; + } + } + } > #qr--modal-message { flex: 1 1 auto; } @@ -312,6 +326,6 @@ } } -#shadow_popup.qr--hide { +.shadow_popup.qr--hide { opacity: 0 !important; } diff --git a/public/scripts/extensions/tts/index.js b/public/scripts/extensions/tts/index.js index 556f6b967..f00641082 100644 --- a/public/scripts/extensions/tts/index.js +++ b/public/scripts/extensions/tts/index.js @@ -19,8 +19,9 @@ const UPDATE_INTERVAL = 1000; let voiceMapEntries = []; let voiceMap = {}; // {charName:voiceid, charName2:voiceid2} -let storedvalue = false; +let talkingHeadState = false; let lastChatId = null; +let lastMessage = null; let lastMessageHash = null; const DEFAULT_VOICE_MARKER = '[Default Voice]'; @@ -67,7 +68,7 @@ export function getPreviewString(lang) { return previewStrings[lang] ?? fallbackPreview; } -let ttsProviders = { +const ttsProviders = { ElevenLabs: ElevenLabsTtsProvider, Silero: SileroTtsProvider, XTTSv2: XTTSTtsProvider, @@ -82,7 +83,6 @@ let ttsProviders = { let ttsProvider; let ttsProviderName; -let ttsLastMessage = null; async function onNarrateOneMessage() { audioElement.src = '/sounds/silence.mp3'; @@ -130,103 +130,13 @@ async function onNarrateText(args, text) { } 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) { + if (!extension_settings.tts.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) { @@ -238,11 +148,11 @@ function talkingAnimation(switchValue) { const apiUrl = getApiUrl(); const animationType = switchValue ? 'start' : 'stop'; - if (switchValue !== storedvalue) { + if (switchValue !== talkingHeadState) { try { console.log(animationType + ' Talking Animation'); doExtrasFetch(`${apiUrl}/api/talkinghead/${animationType}_talking`); - storedvalue = switchValue; // Update the storedvalue to the current switchValue + talkingHeadState = switchValue; } catch (error) { // Handle the error here or simply ignore it to prevent logging } @@ -289,7 +199,6 @@ function debugTtsPlayback() { { 'ttsProviderName': ttsProviderName, 'voiceMap': voiceMap, - 'currentMessageNumber': currentMessageNumber, 'audioPaused': audioPaused, 'audioJobQueue': audioJobQueue, 'currentAudioJob': currentAudioJob, @@ -477,21 +386,12 @@ async function processAudioJobQueue() { 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 @@ -764,26 +664,103 @@ async function onChatChanged() { await resetTtsPlayback(); const voiceMapInit = initVoiceMap(); await Promise.race([voiceMapInit, delay(1000)]); - ttsLastMessage = null; + lastMessage = null; } -async function onChatDeleted() { +async function onMessageEvent(messageId) { + // If TTS is disabled, do nothing + if (!extension_settings.tts.enabled) { + return; + } + + // Auto generation is disabled + if (!extension_settings.tts.auto_generation) { + return; + } + + const context = getContext(); + + // no characters or group selected + if (!context.groupId && context.characterId === undefined) { + return; + } + + // Chat changed + if (context.chatId !== lastChatId) { + lastChatId = context.chatId; + lastMessageHash = getStringHash(context.chat[messageId]?.mes ?? ''); + + // Force to speak on the first message in the new chat + if (context.chat.length === 1) { + lastMessageHash = -1; + } + } + + // clone message object, as things go haywire if message object is altered below (it's passed by reference) + const message = structuredClone(context.chat[messageId]); + const hashNew = getStringHash(message?.mes ?? ''); + + // if no new messages, or same message, or same message hash, do nothing + if (hashNew === lastMessageHash) { + return; + } + + const isLastMessageInCurrent = () => + lastMessage && + typeof lastMessage === 'object' && + message.swipe_id === lastMessage.swipe_id && + message.name === lastMessage.name && + message.is_user === lastMessage.is_user && + message.mes.indexOf(lastMessage.mes) !== -1; + + // if last message within current message, message got extended. only send diff to TTS. + if (isLastMessageInCurrent()) { + const tmp = structuredClone(message); + message.mes = message.mes.replace(lastMessage.mes, ''); + lastMessage = tmp; + } else { + lastMessage = structuredClone(message); + } + + // 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; + lastChatId = context.chatId; + + console.debug(`Adding message from ${message.name} for TTS processing: "${message.mes}"`); + ttsJobQueue.push(message); +} + +async function onMessageDeleted() { 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) ?? ''); + const 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) ?? ''; + lastMessage = context.chat.length ? structuredClone(context.chat[context.chat.length - 1]) : null; // stop any tts playback since message might not exist anymore - await resetTtsPlayback(); + resetTtsPlayback(); } /** @@ -1079,8 +1056,10 @@ $(document).ready(function () { setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL); // Init depends on all the things eventSource.on(event_types.MESSAGE_SWIPED, resetTtsPlayback); eventSource.on(event_types.CHAT_CHANGED, onChatChanged); - eventSource.on(event_types.MESSAGE_DELETED, onChatDeleted); + eventSource.on(event_types.MESSAGE_DELETED, onMessageDeleted); eventSource.on(event_types.GROUP_UPDATED, onChatChanged); + eventSource.on(event_types.MESSAGE_SENT, onMessageEvent); + eventSource.on(event_types.MESSAGE_RECEIVED, onMessageEvent); registerSlashCommand('speak', onNarrateText, ['narrate', 'tts'], '(text) – narrate any text using currently selected character\'s voice. Use voice="Character Name" argument to set other voice from the voice map, example: /speak voice="Donald Duck" Quack!', true, true); document.body.appendChild(audioElement); }); diff --git a/public/scripts/group-chats.js b/public/scripts/group-chats.js index 412f52aaa..3ce9f0d29 100644 --- a/public/scripts/group-chats.js +++ b/public/scripts/group-chats.js @@ -9,9 +9,11 @@ import { saveBase64AsFile, PAGINATION_TEMPLATE, getBase64Async, + resetScrollHeight, + initScrollHeight, } from './utils.js'; import { RA_CountCharTokens, humanizedDateTime, dragElement, favsToHotswap, getMessageTimeStamp } from './RossAscends-mods.js'; -import { loadMovingUIState, sortEntitiesList } from './power-user.js'; +import { power_user, loadMovingUIState, sortEntitiesList } from './power-user.js'; import { chat, @@ -351,6 +353,46 @@ export function getGroupCharacterCards(groupId, characterId) { return null; } + /** + * Runs the macro engine on a text, with custom replace + * @param {string} value Value to replace + * @param {string} fieldName Name of the field + * @param {string} characterName Name of the character + * @returns {string} Replaced text + * */ + function customBaseChatReplace(value, fieldName, characterName) { + if (!value) { + return ''; + } + + // We should do the custom field name replacement first, and then run it through the normal macro engine with provided names + value = value.replace(//gi, fieldName); + return baseChatReplace(value.trim(), name1, characterName); + } + + /** + * Prepares text with prefix/suffix for a character field + * @param {string} value Value to replace + * @param {string} characterName Name of the character + * @param {string} fieldName Name of the field + * @returns {string} Prepared text + * */ + function replaceAndPrepareForJoin(value, characterName, fieldName) { + value = value.trim(); + if (!value) { + return ''; + } + + // Prepare and replace prefixes + const prefix = customBaseChatReplace(group.generation_mode_join_prefix, fieldName, characterName); + const suffix = customBaseChatReplace(group.generation_mode_join_suffix, fieldName, characterName); + const separator = power_user.instruct.wrap ? '\n' : ''; + // Also run the macro replacement on the actual content + value = customBaseChatReplace(value, fieldName, characterName); + + return `${prefix ? prefix + separator : ''}${value}${suffix ? separator + suffix : ''}`; + } + const scenarioOverride = chat_metadata['scenario']; let descriptions = []; @@ -372,16 +414,16 @@ export function getGroupCharacterCards(groupId, characterId) { continue; } - descriptions.push(baseChatReplace(character.description.trim(), name1, character.name)); - personalities.push(baseChatReplace(character.personality.trim(), name1, character.name)); - scenarios.push(baseChatReplace(character.scenario.trim(), name1, character.name)); - mesExamplesArray.push(baseChatReplace(character.mes_example.trim(), name1, character.name)); + descriptions.push(replaceAndPrepareForJoin(character.description, character.name, 'Description')); + personalities.push(replaceAndPrepareForJoin(character.personality, character.name, 'Personality')); + scenarios.push(replaceAndPrepareForJoin(character.scenario, character.name, 'Scenario')); + mesExamplesArray.push(replaceAndPrepareForJoin(character.mes_example, character.name, 'Example Messages')); } - const description = descriptions.join('\n'); - const personality = personalities.join('\n'); - const scenario = scenarioOverride?.trim() || scenarios.join('\n'); - const mesExamples = mesExamplesArray.join('\n'); + const description = descriptions.filter(x => x.length).join('\n'); + const personality = personalities.filter(x => x.length).join('\n'); + const scenario = scenarioOverride?.trim() || scenarios.filter(x => x.length).join('\n'); + const mesExamples = mesExamplesArray.filter(x => x.length).join('\n'); return { description, personality, scenario, mesExamples }; } @@ -1093,6 +1135,8 @@ async function onGroupGenerationModeInput(e) { let _thisGroup = groups.find((x) => x.id == openGroupId); _thisGroup.generation_mode = Number(e.target.value); await editGroup(openGroupId, false, false); + + toggleHiddenControls(_thisGroup); } } @@ -1105,6 +1149,15 @@ async function onGroupAutoModeDelayInput(e) { } } +async function onGroupGenerationModeTemplateInput(e) { + if (openGroupId) { + let _thisGroup = groups.find((x) => x.id == openGroupId); + const prop = $(e.target).attr('setting'); + _thisGroup[prop] = String(e.target.value); + await editGroup(openGroupId, false, false); + } +} + async function onGroupNameInput() { if (openGroupId) { let _thisGroup = groups.find((x) => x.id == openGroupId); @@ -1270,6 +1323,14 @@ async function onHideMutedSpritesClick(value) { } } +function toggleHiddenControls(group, generationMode = null) { + const isJoin = [group_generation_mode.APPEND, group_generation_mode.APPEND_DISABLED].includes(generationMode ?? group?.generation_mode); + $('#rm_group_generation_mode_join_prefix').parent().toggle(isJoin); + $('#rm_group_generation_mode_join_suffix').parent().toggle(isJoin); + initScrollHeight($('#rm_group_generation_mode_join_prefix')); + initScrollHeight($('#rm_group_generation_mode_join_suffix')); +} + function select_group_chats(groupId, skipAnimation) { openGroupId = groupId; newGroupMembers = []; @@ -1305,6 +1366,10 @@ function select_group_chats(groupId, skipAnimation) { $('#rm_group_hidemutedsprites').prop('checked', group && group.hideMutedSprites); $('#rm_group_automode_delay').val(group?.auto_mode_delay ?? DEFAULT_AUTO_MODE_DELAY); + $('#rm_group_generation_mode_join_prefix').val(group?.generation_mode_join_prefix ?? '').attr('setting', 'generation_mode_join_prefix'); + $('#rm_group_generation_mode_join_suffix').val(group?.generation_mode_join_suffix ?? '').attr('setting', 'generation_mode_join_suffix'); + toggleHiddenControls(group, generationMode); + // bottom buttons if (openGroupId) { $('#rm_group_submit').hide(); @@ -1338,6 +1403,11 @@ function select_group_chats(groupId, skipAnimation) { $('#rm_group_automode_label').hide(); } + // Toggle textbox sizes, as input events have not fired here + $('#rm_group_chats_block .autoSetHeight').each(element => { + resetScrollHeight(element); + }); + eventSource.emit('groupSelected', { detail: { id: openGroupId, group: group } }); } @@ -1796,6 +1866,10 @@ function doCurMemberListPopout() { } jQuery(() => { + $(document).on('input', '#rm_group_chats_block .autoSetHeight', function () { + resetScrollHeight($(this)); + }); + $(document).on('click', '.group_select', function () { const groupId = $(this).attr('chid') || $(this).attr('grid') || $(this).data('id'); openGroupById(groupId); @@ -1823,6 +1897,8 @@ jQuery(() => { $('#rm_group_activation_strategy').on('change', onGroupActivationStrategyInput); $('#rm_group_generation_mode').on('change', onGroupGenerationModeInput); $('#rm_group_automode_delay').on('input', onGroupAutoModeDelayInput); + $('#rm_group_generation_mode_join_prefix').on('input', onGroupGenerationModeTemplateInput); + $('#rm_group_generation_mode_join_suffix').on('input', onGroupGenerationModeTemplateInput); $('#group_avatar_button').on('input', uploadGroupAvatar); $('#rm_group_restore_avatar').on('click', restoreGroupAvatar); $(document).on('click', '.group_member .right_menu_button', onGroupActionClick); diff --git a/public/scripts/instruct-mode.js b/public/scripts/instruct-mode.js index 7fc924274..ba1b1506e 100644 --- a/public/scripts/instruct-mode.js +++ b/public/scripts/instruct-mode.js @@ -372,7 +372,7 @@ export function formatInstructModeSystemPrompt(systemPrompt) { * @returns {string[]} Formatted example messages string. */ export function formatInstructModeExamples(mesExamplesArray, name1, name2) { - const blockHeading = power_user.context.example_separator ? power_user.context.example_separator + '\n' : ''; + const blockHeading = power_user.context.example_separator ? `${substituteParams(power_user.context.example_separator)}\n` : ''; if (power_user.instruct.skip_examples) { return mesExamplesArray.map(x => x.replace(/\n/i, blockHeading)); @@ -419,10 +419,13 @@ export function formatInstructModeExamples(mesExamplesArray, name1, name2) { } for (const example of blockExamples) { + // If force group/persona names is set, we should override the include names for the user placeholder + const includeThisName = includeNames || (power_user.instruct.names_force_groups && example.name == 'example_user'); + const prefix = example.name == 'example_user' ? inputPrefix : outputPrefix; const suffix = example.name == 'example_user' ? inputSuffix : outputSuffix; const name = example.name == 'example_user' ? name1 : name2; - const messageContent = includeNames ? `${name}: ${example.content}` : example.content; + const messageContent = includeThisName ? `${name}: ${example.content}` : example.content; const formattedMessage = [prefix, messageContent + suffix].filter(x => x).join(separator); formattedExamples.push(formattedMessage); } @@ -515,13 +518,16 @@ function selectMatchingContextTemplate(name) { /** * Replaces instruct mode macros in the given input string. * @param {string} input Input string. + * @param {Object} env - Map of macro names to the values they'll be substituted with. If the param + * values are functions, those functions will be called and their return values are used. * @returns {string} String with macros replaced. */ -export function replaceInstructMacros(input) { +export function replaceInstructMacros(input, env) { if (!input) { return ''; } const instructMacros = { + 'systemPrompt': (power_user.prefer_character_prompt && env.charPrompt ? env.charPrompt : power_user.instruct.system_prompt), 'instructSystem|instructSystemPrompt': power_user.instruct.system_prompt, 'instructSystemPromptPrefix': power_user.instruct.system_sequence_prefix, 'instructSystemPromptSuffix': power_user.instruct.system_sequence_suffix, diff --git a/public/scripts/macros.js b/public/scripts/macros.js index 504b05596..f89197d3f 100644 --- a/public/scripts/macros.js +++ b/public/scripts/macros.js @@ -1,4 +1,4 @@ -import { chat, main_api, getMaxContextSize, getCurrentChatId } from '../script.js'; +import { chat, chat_metadata, main_api, getMaxContextSize, getCurrentChatId } from '../script.js'; import { timestampToMoment, isDigitsOnly, getStringHash } from './utils.js'; import { textgenerationwebui_banned_in_macros } from './textgen-settings.js'; import { replaceInstructMacros } from './instruct-mode.js'; @@ -8,121 +8,121 @@ import { replaceVariableMacros } from './variables.js'; Handlebars.registerHelper('trim', () => '{{trim}}'); /** - * Returns the ID of the last message in the chat. - * @returns {string} The ID of the last message in the chat. + * Gets a hashed id of the current chat from the metadata. + * If no metadata exists, creates a new hash and saves it. + * @returns {number} The hashed chat id */ -function getLastMessageId() { - const index = chat?.length - 1; +function getChatIdHash() { + const cachedIdHash = chat_metadata['chat_id_hash']; - if (!isNaN(index) && index >= 0) { - return String(index); + // If chat_id_hash is not already set, calculate it + if (!cachedIdHash) { + // Use the main_chat if it's available, otherwise get the current chat ID + const chatId = chat_metadata['main_chat'] ?? getCurrentChatId(); + const chatIdHash = getStringHash(chatId); + chat_metadata['chat_id_hash'] = chatIdHash; + return chatIdHash; } - return ''; + return cachedIdHash; } /** - * Returns the ID of the first message included in the context. - * @returns {string} The ID of the first message in the context. + * Returns the ID of the last message in the chat + * + * Optionally can only choose specific messages, if a filter is provided. + * + * @param {object} param0 - Optional arguments + * @param {boolean} [param0.exclude_swipe_in_propress=true] - Whether a message that is currently being swiped should be ignored + * @param {function(object):boolean} [param0.filter] - A filter applied to the search, ignoring all messages that don't match the criteria. For example to only find user messages, etc. + * @returns {number|null} The message id, or null if none was found + */ +export function getLastMessageId({ exclude_swipe_in_propress = true, filter = null } = {}) { + for (let i = chat?.length - 1; i >= 0; i--) { + let message = chat[i]; + + // If ignoring swipes and the message is being swiped, continue + // We can check if a message is being swiped by checking whether the current swipe id is not in the list of finished swipes yet + if (exclude_swipe_in_propress && message.swipes && message.swipe_id >= message.swipes.length) { + continue; + } + + // Check if no filter is provided, or if the message passes the filter + if (!filter || filter(message)) { + return i; + } + } + + return null; +} + +/** + * Returns the ID of the first message included in the context + * + * @returns {number|null} The ID of the first message in the context */ function getFirstIncludedMessageId() { - const index = document.querySelector('.lastInContext')?.getAttribute('mesid'); + const index = Number(document.querySelector('.lastInContext')?.getAttribute('mesid')); if (!isNaN(index) && index >= 0) { - return String(index); + return index; } - return ''; + return null; } /** - * Returns the last message in the chat. - * @returns {string} The last message in the chat. + * 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 ''; + const mid = getLastMessageId(); + return chat[mid]?.mes ?? ''; } /** - * Returns the last message from the user. - * @returns {string} The last message from the user. + * Returns the last message from the user + * + * @returns {string} The last message from the user */ function getLastUserMessage() { - if (!Array.isArray(chat) || chat.length === 0) { - return ''; - } - - for (let i = chat.length - 1; i >= 0; i--) { - if (chat[i].is_user && !chat[i].is_system) { - return chat[i].mes; - } - } - - return ''; + const mid = getLastMessageId({ filter: m => m.is_user && !m.is_system }); + return chat[mid]?.mes ?? ''; } /** - * Returns the last message from the bot. - * @returns {string} The last message from the bot. + * Returns the last message from the bot + * + * @returns {string} The last message from the bot */ function getLastCharMessage() { - if (!Array.isArray(chat) || chat.length === 0) { - return ''; - } - - for (let i = chat.length - 1; i >= 0; i--) { - if (!chat[i].is_user && !chat[i].is_system) { - return chat[i].mes; - } - } - - return ''; + const mid = getLastMessageId({ filter: m => !m.is_user && !m.is_system }); + return chat[mid]?.mes ?? ''; } /** - * Returns the ID of the last swipe. - * @returns {string} The 1-based ID of the last swipe + * Returns the 1-based ID (number) of the last swipe + * + * @returns {number|null} 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 ''; + // For swipe macro, we are accepting using the message that is currently being swiped + const mid = getLastMessageId({ exclude_swipe_in_propress: false }); + const swipes = chat[mid]?.swipes; + return swipes?.length; } /** - * Returns the ID of the current swipe. - * @returns {string} The 1-based ID of the current swipe. + * Returns the 1-based ID (number) of the current swipe + * + * @returns {number|null} 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 ''; + // For swipe macro, we are accepting using the message that is currently being swiped + const mid = getLastMessageId({ exclude_swipe_in_propress: false }); + const swipeId = chat[mid]?.swipe_id; + return swipeId ? swipeId + 1 : null; } /** @@ -205,7 +205,10 @@ function randomReplace(input, emptyListPlaceholder = '') { function pickReplace(input, rawContent, emptyListPlaceholder = '') { const pickPattern = /{{pick\s?::?([^}]+)}}/gi; - const chatIdHash = getStringHash(getCurrentChatId()); + + // We need to have a consistent chat hash, otherwise we'll lose rolls on chat file rename or branch switches + // No need to save metadata here - branching and renaming will implicitly do the save for us, and until then loading it like this is consistent + const chatIdHash = getChatIdHash(); const rawContentHash = getStringHash(rawContent); return input.replace(pickPattern, (match, listString, offset) => { @@ -276,7 +279,7 @@ export function evaluateMacros(content, env) { } content = diceRollReplace(content); - content = replaceInstructMacros(content); + content = replaceInstructMacros(content, env); content = replaceVariableMacros(content); content = content.replace(/{{newline}}/gi, '\n'); content = content.replace(/\n*{{trim}}\n*/gi, ''); @@ -292,12 +295,12 @@ export function evaluateMacros(content, env) { content = content.replace(/{{maxPrompt}}/gi, () => String(getMaxContextSize())); content = content.replace(/{{lastMessage}}/gi, () => getLastMessage()); - content = content.replace(/{{lastMessageId}}/gi, () => getLastMessageId()); + content = content.replace(/{{lastMessageId}}/gi, () => String(getLastMessageId() ?? '')); content = content.replace(/{{lastUserMessage}}/gi, () => getLastUserMessage()); content = content.replace(/{{lastCharMessage}}/gi, () => getLastCharMessage()); - content = content.replace(/{{firstIncludedMessageId}}/gi, () => getFirstIncludedMessageId()); - content = content.replace(/{{lastSwipeId}}/gi, () => getLastSwipeId()); - content = content.replace(/{{currentSwipeId}}/gi, () => getCurrentSwipeId()); + content = content.replace(/{{firstIncludedMessageId}}/gi, () => String(getFirstIncludedMessageId() ?? '')); + content = content.replace(/{{lastSwipeId}}/gi, () => String(getLastSwipeId() ?? '')); + content = content.replace(/{{currentSwipeId}}/gi, () => String(getCurrentSwipeId() ?? '')); content = content.replace(/\{\{\/\/([\s\S]*?)\}\}/gm, ''); diff --git a/public/scripts/openai.js b/public/scripts/openai.js index 59256df75..19268a005 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -431,15 +431,15 @@ function convertChatCompletionToInstruct(messages, type) { const exampleMessages = messages.filter(x => x.role === 'system' && (x.name === 'example_user' || x.name === 'example_assistant')); if (exampleMessages.length) { - examplesText = power_user.context.example_separator + '\n'; - examplesText += exampleMessages.map(toString).join('\n'); - examplesText = formatInstructModeExamples(examplesText, name1, name2); + const blockHeading = power_user.context.example_separator ? (substituteParams(power_user.context.example_separator) + '\n') : ''; + const examplesArray = exampleMessages.map(m => '\n' + toString(m)); + examplesText = blockHeading + formatInstructModeExamples(examplesArray, name1, name2).join(''); } const chatMessages = messages.slice(firstChatMessage); if (chatMessages.length) { - chatMessagesText = power_user.context.chat_start + '\n'; + chatMessagesText = substituteParams(power_user.context.chat_start) + '\n'; for (const message of chatMessages) { const name = getPrefix(message); diff --git a/public/scripts/popup.js b/public/scripts/popup.js new file mode 100644 index 000000000..b793f3a66 --- /dev/null +++ b/public/scripts/popup.js @@ -0,0 +1,225 @@ +import { animation_duration, animation_easing } from '../script.js'; +import { delay } from './utils.js'; + + + +/**@readonly*/ +/**@enum {Number}*/ +export const POPUP_TYPE = { + 'TEXT': 1, + 'CONFIRM': 2, + 'INPUT': 3, +}; + +/**@readonly*/ +/**@enum {Boolean}*/ +export const POPUP_RESULT = { + 'AFFIRMATIVE': true, + 'NEGATIVE': false, + 'CANCELLED': undefined, +}; + + + +export class Popup { + /**@type {POPUP_TYPE}*/ type; + + /**@type {HTMLElement}*/ dom; + /**@type {HTMLElement}*/ dlg; + /**@type {HTMLElement}*/ text; + /**@type {HTMLTextAreaElement}*/ input; + /**@type {HTMLElement}*/ ok; + /**@type {HTMLElement}*/ cancel; + + /**@type {POPUP_RESULT}*/ result; + /**@type {any}*/ value; + + /**@type {Promise}*/ promise; + /**@type {Function}*/ resolver; + + /**@type {Function}*/ keyListenerBound; + + + + /** + * @typedef {{okButton?: string, cancelButton?: string, rows?: number, wide?: boolean, large?: boolean, allowHorizontalScrolling?: boolean, allowVerticalScrolling?: boolean }} PopupOptions - Options for the popup. + * @param {JQuery|string|Element} text - Text to display in the popup. + * @param {POPUP_TYPE} type - One of Popup.TYPE + * @param {string} inputValue - Value to set the input to. + * @param {PopupOptions} options - Options for the popup. + */ + constructor(text, type, inputValue = '', { okButton, cancelButton, rows, wide, large, allowHorizontalScrolling, allowVerticalScrolling } = {}) { + this.type = type; + + /**@type {HTMLTemplateElement}*/ + const template = document.querySelector('#shadow_popup_template'); + // @ts-ignore + this.dom = template.content.cloneNode(true).querySelector('.shadow_popup'); + const dlg = this.dom.querySelector('.dialogue_popup'); + // @ts-ignore + this.dlg = dlg; + this.text = this.dom.querySelector('.dialogue_popup_text'); + this.input = this.dom.querySelector('.dialogue_popup_input'); + this.ok = this.dom.querySelector('.dialogue_popup_ok'); + this.cancel = this.dom.querySelector('.dialogue_popup_cancel'); + + if (wide) dlg.classList.add('wide_dialogue_popup'); + if (large) dlg.classList.add('large_dialogue_popup'); + if (allowHorizontalScrolling) dlg.classList.add('horizontal_scrolling_dialogue_popup'); + if (allowVerticalScrolling) dlg.classList.add('vertical_scrolling_dialogue_popup'); + + this.ok.textContent = okButton ?? 'OK'; + this.cancel.textContent = cancelButton ?? 'Cancel'; + + switch(type) { + case POPUP_TYPE.TEXT: { + this.input.style.display = 'none'; + this.cancel.style.display = 'none'; + break; + } + case POPUP_TYPE.CONFIRM: { + this.input.style.display = 'none'; + this.ok.textContent = okButton ?? 'Yes'; + this.cancel.textContent = cancelButton ?? 'No'; + break; + } + case POPUP_TYPE.INPUT: { + this.input.style.display = 'block'; + this.ok.textContent = okButton ?? 'Save'; + break; + } + default: { + // illegal argument + } + } + + this.input.value = inputValue; + this.input.rows = rows ?? 1; + + this.text.innerHTML = ''; + if (text instanceof jQuery) { + $(this.text).append(text); + } else if (text instanceof HTMLElement) { + this.text.append(text); + } else if (typeof text == 'string') { + this.text.innerHTML = text; + } else { + // illegal argument + } + + this.ok.addEventListener('click', ()=>this.completeAffirmative()); + this.cancel.addEventListener('click', ()=>this.completeNegative()); + const keyListener = (evt)=>{ + switch (evt.key) { + case 'Escape': { + evt.preventDefault(); + evt.stopPropagation(); + this.completeCancelled(); + window.removeEventListener('keydown', keyListenerBound); + break; + } + } + }; + const keyListenerBound = keyListener.bind(this); + window.addEventListener('keydown', keyListenerBound); + } + + async show() { + document.body.append(this.dom); + this.dom.style.display = 'block'; + switch(this.type) { + case POPUP_TYPE.INPUT: { + this.input.focus(); + break; + } + } + + $(this.dom).transition({ + opacity: 1, + duration: animation_duration, + easing: animation_easing, + }); + + this.promise = new Promise((resolve) => { + this.resolver = resolve; + }); + return this.promise; + } + + completeAffirmative() { + switch (this.type) { + case POPUP_TYPE.TEXT: + case POPUP_TYPE.CONFIRM: { + this.value = true; + break; + } + case POPUP_TYPE.INPUT: { + this.value = this.input.value; + break; + } + } + this.result = POPUP_RESULT.AFFIRMATIVE; + this.hide(); + } + + completeNegative() { + switch (this.type) { + case POPUP_TYPE.TEXT: + case POPUP_TYPE.CONFIRM: + case POPUP_TYPE.INPUT: { + this.value = false; + break; + } + } + this.result = POPUP_RESULT.NEGATIVE; + this.hide(); + } + + completeCancelled() { + switch (this.type) { + case POPUP_TYPE.TEXT: + case POPUP_TYPE.CONFIRM: + case POPUP_TYPE.INPUT: { + this.value = null; + break; + } + } + this.result = POPUP_RESULT.CANCELLED; + this.hide(); + } + + + + hide() { + $(this.dom).transition({ + opacity: 0, + duration: animation_duration, + easing: animation_easing, + }); + delay(animation_duration).then(()=>{ + this.dom.remove(); + }); + + this.resolver(this.value); + } +} + + + +/** + * Displays a blocking popup with a given text and type. + * @param {JQuery|string|Element} text - Text to display in the popup. + * @param {POPUP_TYPE} type + * @param {string} inputValue - Value to set the input to. + * @param {PopupOptions} options - Options for the popup. + * @returns + */ +export function callGenericPopup(text, type, inputValue = '', { okButton, cancelButton, rows, wide, large, allowHorizontalScrolling, allowVerticalScrolling } = {}) { + const popup = new Popup( + text, + type, + inputValue, + { okButton, rows, wide, large, allowHorizontalScrolling, allowVerticalScrolling }, + ); + return popup.show(); +} diff --git a/public/scripts/tags.js b/public/scripts/tags.js index d08b170f4..b1c40c020 100644 --- a/public/scripts/tags.js +++ b/public/scripts/tags.js @@ -708,7 +708,7 @@ function printTagList(element, { tags = undefined, addTag = undefined, forEntity const id = 'placeholder_' + uuidv4(); // Add click event - const showHiddenTags = (event) => { + const showHiddenTags = (_, event) => { const elementKey = key ?? getTagKeyForEntityElement($element); console.log(`Hidden tags shown for element ${elementKey}`); @@ -765,7 +765,7 @@ function appendTagToList(listElement, tag, { removable = false, selectable = fal tagElement.attr('title', tag.title); } if (tag.icon) { - tagElement.find('.tag_name').text('').attr('title', `${tag.name} ${tag.title}`.trim()).addClass(tag.icon); + tagElement.find('.tag_name').text('').attr('title', `${tag.name} ${tag.title || ''}`.trim()).addClass(tag.icon); tagElement.addClass('actionable'); } @@ -783,7 +783,7 @@ function appendTagToList(listElement, tag, { removable = false, selectable = fal if (clickableAction) { const filter = getFilterHelper($(listElement)); - tagElement.on('click', (e) => clickableAction.bind(tagElement)(e, filter)); + tagElement.on('click', (e) => clickableAction.bind(tagElement)(filter, e)); tagElement.addClass('clickable-action'); } diff --git a/public/scripts/templates/macros.html b/public/scripts/templates/macros.html index f3291333f..2eb9ca7b7 100644 --- a/public/scripts/templates/macros.html +++ b/public/scripts/templates/macros.html @@ -49,6 +49,7 @@
  • {{maxPrompt}} – max allowed prompt length in tokens = (context size - response length)
  • {{exampleSeparator}} – context template example dialogues separator
  • {{chatStart}} – context template chat start line
  • +
  • {{systemPrompt}} – main system prompt (either character prompt override if chosen, or instructSystemPrompt)
  • {{instructSystemPrompt}} – instruct system prompt
  • {{instructSystemPromptPrefix}} – instruct system prompt prefix sequence
  • {{instructSystemPromptSuffix}} – instruct system prompt suffix sequence
  • diff --git a/public/style.css b/public/style.css index 2a49af5db..d9818d537 100644 --- a/public/style.css +++ b/public/style.css @@ -2059,7 +2059,8 @@ grammarly-extension { /* Focus */ #bulk_tag_popup, -#dialogue_popup { +#dialogue_popup, +.dialogue_popup { width: 500px; max-width: 90vw; max-width: 90svw; @@ -2112,7 +2113,8 @@ grammarly-extension { } #bulk_tag_popup_holder, -#dialogue_popup_holder { +#dialogue_popup_holder, +.dialogue_popup_holder { display: flex; flex-direction: column; height: 100%; @@ -2120,13 +2122,15 @@ grammarly-extension { padding: 0 10px; } -#dialogue_popup_text { +#dialogue_popup_text, +.dialogue_popup_text { flex-grow: 1; overflow-y: auto; height: 100%; } -#dialogue_popup_controls { +#dialogue_popup_controls, +.dialogue_popup_controls { display: flex; align-self: center; gap: 20px; @@ -2134,14 +2138,16 @@ grammarly-extension { #bulk_tag_popup_reset, #bulk_tag_popup_remove_mutual, -#dialogue_popup_ok { +#dialogue_popup_ok, +.dialogue_popup_ok { background-color: var(--crimson70a); cursor: pointer; } #bulk_tag_popup_reset:hover, #bulk_tag_popup_remove_mutual:hover, -#dialogue_popup_ok:hover { +#dialogue_popup_ok:hover, +.dialogue_popup_ok:hover { background-color: var(--crimson-hover); } @@ -2149,13 +2155,15 @@ grammarly-extension { max-height: 70vh; } -#dialogue_popup_input { +#dialogue_popup_input, +.dialogue_popup_input { margin: 10px 0; width: 100%; } #bulk_tag_popup_cancel, -#dialogue_popup_cancel { +#dialogue_popup_cancel, +.dialogue_popup_cancel { cursor: pointer; } @@ -2220,7 +2228,7 @@ grammarly-extension { margin-right: 25px; } -#shadow_popup { +#shadow_popup, .shadow_popup { backdrop-filter: blur(calc(var(--SmartThemeBlurStrength) * 2)); -webkit-backdrop-filter: blur(calc(var(--SmartThemeBlurStrength) * 2)); background-color: var(--black30a); @@ -2232,6 +2240,9 @@ grammarly-extension { height: 100svh; z-index: 9999; top: 0; + &.shadow_popup { + z-index: 9998; + } } #bgtest { diff --git a/server.js b/server.js index 2b8b2870e..2a29f950d 100644 --- a/server.js +++ b/server.js @@ -364,6 +364,7 @@ redirect('/savequickreply', '/api/quick-replies/save'); // Redirect deprecated image endpoints redirect('/uploadimage', '/api/images/upload'); redirect('/listimgfiles/:folder', '/api/images/list/:folder'); +redirect('/api/content/import', '/api/content/importURL'); // Redirect deprecated moving UI endpoints redirect('/savemovingui', '/api/moving-ui/save'); diff --git a/src/endpoints/groups.js b/src/endpoints/groups.js index ac83cb06b..a69ac9b1c 100644 --- a/src/endpoints/groups.js +++ b/src/endpoints/groups.js @@ -73,6 +73,8 @@ router.post('/create', jsonParser, (request, response) => { chat_id: request.body.chat_id ?? id, chats: request.body.chats ?? [id], auto_mode_delay: request.body.auto_mode_delay ?? 5, + generation_mode_join_prefix: request.body.generation_mode_join_prefix ?? '', + generation_mode_join_suffix: request.body.generation_mode_join_suffix ?? '', }; const pathToFile = path.join(request.user.directories.groups, `${id}.json`); const fileData = JSON.stringify(groupMetadata);