Merge branch 'staging' into parser-v2

This commit is contained in:
LenAnderson
2024-04-09 13:29:41 -04:00
17 changed files with 457 additions and 167 deletions

1
.gitignore vendored
View File

@ -45,3 +45,4 @@ access.log
/cache/
public/css/user.css
/plugins/
/data

View File

@ -4519,6 +4519,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

@ -211,6 +211,7 @@ import { loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermati
import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId } from './scripts/chats.js';
import { initPresetManager } from './scripts/preset-manager.js';
import { evaluateMacros } from './scripts/macros.js';
import { callGenericPopup } from './scripts/popup.js';
//exporting functions and vars for mods
export {
@ -821,7 +822,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';
@ -3466,7 +3467,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);
@ -3513,10 +3513,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,
@ -3784,12 +3785,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);
@ -4025,7 +4027,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 || ''),
@ -4649,8 +4652,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');
@ -7808,6 +7816,7 @@ window['SillyTavern'].getContext = function () {
registedDebugFunction: registerDebugFunction,
renderExtensionTemplate: renderExtensionTemplate,
callPopup: callPopup,
callGenericPopup: callGenericPopup,
mainApi: main_api,
extensionSettings: extension_settings,
ModuleWorkerWrapper: ModuleWorkerWrapper,

View File

@ -181,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>')
@ -188,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="Extension repo/guide: ${url}">
<a class="asset_preview" href="${url}" target="_blank" title="${title}">
<i class="fa-solid fa-sm ${previewIcon}"></i>
</a>
</span>

View File

@ -1,4 +1,4 @@
import { callPopup } from '../../../../script.js';
import { POPUP_TYPE, Popup } from '../../../popup.js';
import { setSlashCommandAutoComplete } from '../../../slash-commands.js';
import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
import { getSortableDelay } from '../../../utils.js';
@ -46,6 +46,8 @@ export class QuickReply {
/**@type {HTMLInputElement}*/ settingsDomLabel;
/**@type {HTMLTextAreaElement}*/ settingsDomMessage;
/**@type {Popup}*/ editorPopup;
/**@type {HTMLElement}*/ editorExecuteBtn;
/**@type {HTMLElement}*/ editorExecuteErrors;
/**@type {HTMLInputElement}*/ editorExecuteHide;
@ -199,7 +201,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}*/
@ -446,7 +449,7 @@ export class QuickReply {
this.editorExecuteBtn.classList.add('qr--busy');
this.editorExecuteErrors.innerHTML = '';
if (this.editorExecuteHide.checked) {
document.querySelector('#shadow_popup').classList.add('qr--hide');
this.editorPopup.dom.classList.add('qr--hide');
}
try {
this.editorExecutePromise = this.execute();
@ -456,7 +459,7 @@ export class QuickReply {
}
this.editorExecutePromise = null;
this.editorExecuteBtn.classList.remove('qr--busy');
document.querySelector('#shadow_popup').classList.remove('qr--hide');
this.editorPopup.dom.classList.remove('qr--hide');
}

View File

@ -216,60 +216,60 @@
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-editorSettings {
.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;
@ -277,24 +277,24 @@
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 {
.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 {
.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 {
.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;
@ -326,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

@ -353,14 +353,30 @@ export function getGroupCharacterCards(groupId, characterId) {
return null;
}
/** Runs the macro engine on a text, with custom <FIELDNAME> replace @param {string} value @param {string} characterName @param {string} fieldName @returns {string} */
/**
* 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 @param {string} characterName @param {string} fieldName @returns {string} */
/**
* 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) {

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));
@ -518,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';
@ -7,6 +7,25 @@ import { replaceVariableMacros } from './variables.js';
// Register any macro that you want to leave in the compiled story string
Handlebars.registerHelper('trim', () => '{{trim}}');
/**
* 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 getChatIdHash() {
const cachedIdHash = chat_metadata['chat_id_hash'];
// 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 cachedIdHash;
}
/**
* Returns the ID of the last message in the chat
@ -186,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) => {
@ -257,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, '');

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

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

@ -2136,7 +2136,8 @@ grammarly-extension {
/* Focus */
#bulk_tag_popup,
#dialogue_popup {
#dialogue_popup,
.dialogue_popup {
width: 500px;
max-width: 90vw;
max-width: 90svw;
@ -2189,7 +2190,8 @@ grammarly-extension {
}
#bulk_tag_popup_holder,
#dialogue_popup_holder {
#dialogue_popup_holder,
.dialogue_popup_holder {
display: flex;
flex-direction: column;
height: 100%;
@ -2197,13 +2199,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;
@ -2211,14 +2215,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);
}
@ -2226,13 +2232,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;
}
@ -2297,7 +2305,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);
@ -2309,6 +2317,9 @@ grammarly-extension {
height: 100svh;
z-index: 9999;
top: 0;
&.shadow_popup {
z-index: 9998;
}
}
#bgtest {

View File

@ -341,6 +341,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

@ -74,6 +74,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(DIRECTORIES.groups, `${id}.json`);
const fileData = JSON.stringify(groupMetadata);