Merge branch 'staging' into neo-server

This commit is contained in:
Cohee
2024-04-09 20:26:03 +03:00
21 changed files with 769 additions and 291 deletions

View File

@@ -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;

View File

@@ -4375,24 +4375,44 @@
</div>
<div name="GroupStragegyAndOrder" id="rm_group_buttons" class="flex-container paddingLeftRight5 flex2">
<div class="flex1 flexGap5">
<div class="flex-container flexnowrap width100p whitespacenowrap">
<label for="rm_group_activation_strategy" class="flexnowrap width100p whitespacenowrap">
<span data-i18n="Group reply strategy">Group reply strategy</span>
</div>
</label>
<select id="rm_group_activation_strategy">
<option value="0" data-i18n="Natural order">Natural order</option>
<option value="1" data-i18n="List order">List order</option>
</select>
</div>
<div class="flex1 flexGap5">
<div class="flex-container flexnowrap width100p whitespacenowrap">
<label for="rm_group_generation_mode" class="flexnowrap width100p whitespacenowrap">
<span data-i18n="Group generation handling mode">Group generation handling mode</span>
</div>
</label>
<select id="rm_group_generation_mode">
<option value="0" data-i18n="Swap character cards">Swap character cards</option>
<option value="1" data-i18n="Join character cards (exclude muted)">Join character cards (exclude muted)</option>
<option value="2" data-i18n="Join character cards (include muted)">Join character cards (include muted)</option>
</select>
</div>
<div class="flex1 flexGap5" title="Inserted before each part of the joined fields.">
<label for="rm_group_generation_mode_join_prefix" class="flexnowrap width100p whitespacenowrap">
<span data-i18n="Join Prefix">Join Prefix</span>
<div class="fa-solid fa-circle-info opacity50p"
data-i18n="[title]When 'Join character cards' is selected, all respective fields of the characters are being joined together.&#13;This means that in the story string for example all character descriptions will be joined to one big text.&#13;If you want those fields to be separated, you can define a prefix or suffix here.&#13;&#13;This value supports normal macros and will also replace {{char}} with the relevant char's name and &lt;FIELDNAME&gt; with the name of the part (e.g.: description, personality, scenario, etc.)"
title="When 'Join character cards' is selected, all respective fields of the characters are being joined together.&#13;This means that in the story string for example all character descriptions will be joined to one big text.&#13;If you want those fields to be separated, you can define a prefix or suffix here.&#13;&#13;This value supports normal macros and will also replace {{char}} with the relevant char's name and &lt;FIELDNAME&gt; with the name of the part (e.g.: description, personality, scenario, etc.)">
</div>
</label>
<textarea id="rm_group_generation_mode_join_prefix" class="text_pole wide100p textarea_compact autoSetHeight" maxlength="2000" placeholder="&mdash;" rows="1"></textarea>
</div>
<div class="flex1 flexGap5" title="Inserted after each part of the joined fields.">
<label for="rm_group_generation_mode_join_suffix" class="flexnowrap width100p whitespacenowrap">
<span data-i18n="Join Suffix">Join Suffix</span>
<div class="fa-solid fa-circle-info opacity50p"
data-i18n="[title]When 'Join character cards' is selected, all respective fields of the characters are being joined together.&#13;This means that in the story string for example all character descriptions will be joined to one big text.&#13;If you want those fields to be separated, you can define a prefix or suffix here.&#13;&#13;This value supports normal macros and will also replace {{char}} with the relevant char's name and &lt;FIELDNAME&gt; with the name of the part (e.g.: description, personality, scenario, etc.)"
title="When 'Join character cards' is selected, all respective fields of the characters are being joined together.&#13;This means that in the story string for example all character descriptions will be joined to one big text.&#13;If you want those fields to be separated, you can define a prefix or suffix here.&#13;&#13;This value supports normal macros and will also replace {{char}} with the relevant char's name and &lt;FIELDNAME&gt; with the name of the part (e.g.: description, personality, scenario, etc.)">
</div>
</label>
<textarea id="rm_group_generation_mode_join_suffix" class="text_pole wide100p textarea_compact autoSetHeight" maxlength="2000" placeholder="&mdash;" rows="1"></textarea>
</div>
</div>
<div id="GroupFavDelOkBack" class="flex-container flexGap5 spaceEvenly flex1">
<div id="rm_button_back_from_group" class="heightFitContent margin0 menu_button fa-solid fa-left-long"></div>
@@ -4506,6 +4526,22 @@
</div>
</div>
<!-- various fullscreen popups -->
<template id="shadow_popup_template">
<div class="shadow_popup">
<div class="dialogue_popup">
<div class="dialogue_popup_holder">
<div class="dialogue_popup_text">
<h3 class="margin0">text</h3>
</div>
<textarea class="dialogue_popup_input text_pole" rows="1"></textarea>
<div class="dialogue_popup_controls">
<div class="dialogue_popup_ok menu_button" data-i18n="Delete">Delete</div>
<div class="dialogue_popup_cancel menu_button" data-i18n="Cancel">Cancel</div>
</div>
</div>
</div>
</div>
</template>
<div id="shadow_popup">
<div id="dialogue_popup">
<div id="dialogue_popup_holder">

View File

@@ -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,

View File

@@ -103,7 +103,8 @@ function downloadAssetsList(url) {
if (assetType == 'extension') {
assetTypeMenu.append(`
<div class="assets-list-git">
To download extensions from this page, you need to have <a href="https://git-scm.com/downloads" target="_blank">Git</a> installed.
To download extensions from this page, you need to have <a href="https://git-scm.com/downloads" target="_blank">Git</a> installed.<br>
Click the <i class="fa-solid fa-sm fa-arrow-up-right-from-square"></i> icon to visit the Extension's repo for tips on how to use it.
</div>`);
}
@@ -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 = $('<i></i>')
@@ -187,7 +189,7 @@ function downloadAssetsList(url) {
.append(`<div class="flex-container flexFlowColumn flexNoGap">
<span class="asset-name flex-container alignitemscenter">
<b>${displayName}</b>
<a class="asset_preview" href="${url}" target="_blank" title="Preview in browser">
<a class="asset_preview" href="${url}" target="_blank" title="${title}">
<i class="fa-solid fa-sm ${previewIcon}"></i>
</a>
</span>

View File

@@ -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 = `
<div id="expression-wrapper">
@@ -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);

View File

@@ -18,6 +18,11 @@
<input id="image_type_toggle" type="checkbox">
<span>Image Type - talkinghead (extras)</span>
</label>
<div class="expression_fallback_block m-b-1 m-t-1">
<label for="expression_fallback">Default / Fallback Expression</label>
<small>Set the default and fallback expression being used when no matching expression is found.</small>
<select id="expression_fallback" class="flex1 margin0" data-i18n="Fallback Expression" placeholder="Fallback Expression"></select>
</div>
<div class="expression_custom_block m-b-1 m-t-1">
<label for="expression_custom">Custom Expressions</label>
<small>Can be set manually or with an <tt>/emote</tt> slash command.</small>

View File

@@ -16,12 +16,20 @@
<label for="qr--modal-message">
Message / Command:
</label>
<small>
<div class="qr--modal-editorSettings">
<label class="checkbox_label">
<input type="checkbox" id="qr--modal-wrap">
<span>Word wrap</span>
</label>
</small>
<label class="checkbox_label">
<span>Tab size:</span>
<input type="number" min="1" max="9" id="qr--modal-tabSize" class="text_pole">
</label>
<label class="checkbox_label">
<input type="checkbox" id="qr--modal-executeShortcut">
<span>Ctrl+Enter to execute</span>
</label>
</div>
<textarea class="monospace" id="qr--modal-message"></textarea>
</div>
</div>

View File

@@ -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');
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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'], '<span class="monospace">(text)</span> narrate any text using currently selected character\'s voice. Use voice="Character Name" argument to set other voice from the voice map, example: <tt>/speak voice="Donald Duck" Quack!</tt>', true, true);
document.body.appendChild(audioElement);
});

View File

@@ -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 <FIELDNAME> 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(/<FIELDNAME>/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);

View File

@@ -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(/<START>\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<string, *>} 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,

View File

@@ -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, '');

View File

@@ -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 => '<START>\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);

225
public/scripts/popup.js Normal file
View File

@@ -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<HTMLElement>|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<HTMLElement>|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();
}

View File

@@ -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');
}

View File

@@ -49,6 +49,7 @@
<li><tt>&lcub;&lcub;maxPrompt&rcub;&rcub;</tt> max allowed prompt length in tokens = (context size - response length)</li>
<li><tt>&lcub;&lcub;exampleSeparator&rcub;&rcub;</tt> context template example dialogues separator</li>
<li><tt>&lcub;&lcub;chatStart&rcub;&rcub;</tt> context template chat start line</li>
<li><tt>&lcub;&lcub;systemPrompt&rcub;&rcub;</tt> main system prompt (either character prompt override if chosen, or instructSystemPrompt)</li>
<li><tt>&lcub;&lcub;instructSystemPrompt&rcub;&rcub;</tt> instruct system prompt</li>
<li><tt>&lcub;&lcub;instructSystemPromptPrefix&rcub;&rcub;</tt> instruct system prompt prefix sequence</li>
<li><tt>&lcub;&lcub;instructSystemPromptSuffix&rcub;&rcub;</tt> instruct system prompt suffix sequence</li>

View File

@@ -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 {

View File

@@ -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');

View File

@@ -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);