mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Merge branch 'staging' into neo-server
This commit is contained in:
@@ -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;
|
||||
|
@@ -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. This means that in the story string for example all character descriptions will be joined to one big text. If you want those fields to be separated, you can define a prefix or suffix here. This value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> 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. This means that in the story string for example all character descriptions will be joined to one big text. If you want those fields to be separated, you can define a prefix or suffix here. This value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> 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="—" 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. This means that in the story string for example all character descriptions will be joined to one big text. If you want those fields to be separated, you can define a prefix or suffix here. This value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> 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. This means that in the story string for example all character descriptions will be joined to one big text. If you want those fields to be separated, you can define a prefix or suffix here. This value supports normal macros and will also replace {{char}} with the relevant char's name and <FIELDNAME> 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="—" 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">
|
||||
|
@@ -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,
|
||||
|
@@ -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>
|
||||
|
@@ -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);
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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);
|
||||
});
|
||||
|
@@ -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);
|
||||
|
@@ -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,
|
||||
|
@@ -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, '');
|
||||
|
||||
|
@@ -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
225
public/scripts/popup.js
Normal 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();
|
||||
}
|
@@ -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');
|
||||
}
|
||||
|
||||
|
@@ -49,6 +49,7 @@
|
||||
<li><tt>{{maxPrompt}}</tt> – max allowed prompt length in tokens = (context size - response length)</li>
|
||||
<li><tt>{{exampleSeparator}}</tt> – context template example dialogues separator</li>
|
||||
<li><tt>{{chatStart}}</tt> – context template chat start line</li>
|
||||
<li><tt>{{systemPrompt}}</tt> – main system prompt (either character prompt override if chosen, or instructSystemPrompt)</li>
|
||||
<li><tt>{{instructSystemPrompt}}</tt> – instruct system prompt</li>
|
||||
<li><tt>{{instructSystemPromptPrefix}}</tt> – instruct system prompt prefix sequence</li>
|
||||
<li><tt>{{instructSystemPromptSuffix}}</tt> – instruct system prompt suffix sequence</li>
|
||||
|
@@ -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 {
|
||||
|
@@ -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');
|
||||
|
@@ -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);
|
||||
|
Reference in New Issue
Block a user